Linux IO多路复用

Linux 网络中的 I/O 多路复用机制是一种高效处理多个 I/O 事件的模型,它允许单个线程或进程同时监视多个文件描述符(如套接字),并在其中任何一个文件描述符就绪时进行处理。其核心原理如下:

  1. 基本思想:

    • I/O 多路复用通过一个系统调用(如 selectpollepoll)同时监视多个文件描述符,避免了为每个文件描述符创建单独的线程或进程的开销。
    • 当某个文件描述符就绪(如数据可读、可写或异常)时,多路复用机制会通知应用程序进行处理。
  2. 核心机制:

    • 文件描述符集合: 多路复用机制维护一个文件描述符集合,应用程序通过系统调用告诉内核需要监视哪些文件描述符。
    • 事件等待: 内核会挂起调用线程,直到至少有一个文件描述符就绪,或者超时。
    • 事件通知: 当有文件描述符就绪时,内核会唤醒调用线程,并返回就绪的文件描述符集合。
  3. 常用实现:

    • select:
      • 使用位掩码表示文件描述符集合,可以监视的文件描述符数量有限(通常为 1024)。
      • 每次调用需要将整个文件描述符集合从用户空间复制到内核空间,效率较低。
      • 返回时,整个集合被修改,应用程序需要遍历所有文件描述符以确定哪些就绪。
    • poll:
      • 使用 pollfd 结构体表示文件描述符集合,可以监视的文件描述符数量没有限制。
      • 解决了 select 的文件描述符数量限制问题,但仍需要将整个集合从用户空间复制到内核空间。
      • 返回时,需要对整个集合进行遍历以确定哪些文件描述符就绪。
    • epoll:
      • 基于事件驱动的模型,是目前 Linux 上最常用的 I/O 多路复用机制。
      • 使用 epoll_create 创建一个 epoll 实例,然后通过 epoll_ctl 向其中添加或删除文件描述符。
      • 通过 epoll_wait 等待事件,内核只返回就绪的文件描述符,大大减少了遍历的开销。
      • 支持边缘触发(ET)和水平触发(LT)模式,边缘触发模式可以减少事件通知的次数,进一步提高效率。
  4. 性能对比:

    • selectpoll 在文件描述符数量较多时性能较差,因为每次调用都需要遍历整个集合。
    • epoll 在文件描述符数量较多时性能更优,因为它只返回就绪的文件描述符,且不需要每次都复制整个集合。
  5. 适用场景:

    • I/O 多路复用适用于需要同时处理多个 I/O 事件的场景,如网络服务器、实时通信系统等。
    • 特别是 epoll,在大规模并发连接的情况下,性能优势尤为明显。

总结来说,I/O 多路复用机制通过高效地监视和处理多个文件描述符,极大地提升了 I/O 密集型应用程序的性能和可扩展性

MongoDB学习笔记

MongoDB 与关系型数据库的区别

  1. 数据模型

    • MongoDB 使用文档存储,每个文档采用 BSON 格式,能够嵌套结构化数组和对象,无需预先定义固定模式
    • 关系型数据库采用表格结构,数据以行和列形式存储,通常需要预定义格式
  2. 查询语言

    • MongoDB 使用基于 JSON 的查询语言,采用聚合管道实现复杂数据转化,查询灵活性高
    • 关系型数据库使用结构化查询语言 SQL, 但复杂查询可能影响性能
  3. 扩展性

    • MongoDB 天生支持水平扩展,可以跨多态服务器分布存储数据,适合大规模数据和高并发场景。
    • 关系型数据库通常依赖垂直扩展,不如 MongoDB 灵活
  4. 一致性与事务

    • MongoDB 在单文档操作上提供原子性,多文档事务从 V4.0 开始支持,但与关系型数据库相比仍有限。
    • 关系型数据库全面支持 ACID 事务,适合对数据一致性要求极高的场景。

MongoDB 的事务

  1. 性能: 强一致性事务会影响数据库的性能,而 mongoDB 的设计目标是高性能。
  2. 数据模型: 文档型数据库的数据模型相对灵活,但同时也增加了实现事务的复杂性。
  3. 使用场景: MongoDB 更适合用于存储非结构化数据和高并发读写场景,这些场景对事务的要求相对较低。
  4. 一致性: mongoDB 为了性能采用的是最终一致性,需要一段时间才能被所有节点看到,关系型数据库是强一致性。

在设计数据模型时,仍建议尽量利用单文档操作的原子性,只有在必要时才引入跨多文档的事务。

MongoDB 的索引类型有哪些?各自的优缺点是什么?

  1. 单字段索引(single Field)

    • 优点: 简单高效,适用精确匹配/排序
    • 缺点: 只优化单字段查询
  2. 复合索引(Compound Index)

    • 优点: 支持多字段联合查询/排序
    • 缺点: 仅最左前缀生效,占用更多内存
  3. 多键索引(MultiKey)

    • 优点: 自动为数组字段每个元素创建索引
    • 缺点: 大规模数组影响写入性能
  4. 文本索引(Text)

    • 优点: 支持全文搜索(含语言分词)
    • 缺点: 仅一个文本索引/权重管理复杂
  5. 地理空间索引(Geospatial)

    • 优点: 高效处理位置查询
    • 缺点: 仅适用地理坐标数据
  6. 哈希索引(Hashed)

    • 优点: 均匀分片支持, 快速等值查询
    • 缺点: 无法范围查询
  7. 通配符索引(Wildcard)

    • 优点: 动态字段查询优化
    • 缺点: 索引尺寸较大

MongoDB 的查询分析计划

db.collection.find().explain("executionStats") // 执行统计

executionStats 实际执行性能指标

1
2
3
4
5
6
"executionStats": {
"nReturned" : 5, // 返回文档数
"executionTimeMillis" : 2, // 执行时间(ms)
"totalKeysExamined" : 5, // 索引扫描次数
"totalDocsExamined" : 5 // 文档扫描次数
}

理想情况下 nReturned ≈ totalKeysExamined ≈ totalDocsExamined

COLLSCAN(全表扫描) vs IXSCAN(索引扫描)
▶︎ 应尽量避免出现 COLLSCAN

如何保证 MongoDB 的数据一致性?

  1. 写入层保障
1
2
3
4
5
6
7
8
// 强一致性写入配置
db.orders.insertOne({...}, {
writeConcern: {
w: "majority", // 确保数据写入大多数节点
j: true, // 写入journal日志保证持久性
wtimeout: 5000 // 超时设置(毫秒)
}
})
  1. 读取层控制
1
2
// 读取已提交的数据
db.products.find().readConcern("majority");

MongoDB 核心概念

  1. 为什么选择 MongoDB 而不是关系型数据库?

    • MongoDB 的文档模型更灵活,适合快速迭代的开发模式。
    • 它能处理大规模数据和高并发查询,支持水平扩展。
    • 适用于非结构化或半结构化数据,提供更高的开发效率。
  2. MongoDB 的文档模型与传统的关系模型有什么区别?

    • 在 MongoDB 中,数据存储为BSON 格式的文档,类似于 JSON,而关系型数据库使用表格和行。
    • 文档可以嵌套,支持复杂的数据结构,而关系型数据库需要多表关联。
  3. 什么是 BSON?它与 JSON 的区别是什么?

    • BSON(Binary JSON)是 JSON 的二进制表示形式,支持更多的数据类型(如日期、二进制数据)。
    • BSON 比 JSON 更高效,适合存储和传输。

数据操作与查询

  1. 如何执行 CRUD 操作?

    • CreateinsertOne()insertMany()
    • Readfind() 用于查询。
    • UpdateupdateOne()updateMany()
    • DeletedeleteOne()deleteMany()
  2. 什么是聚合管道?如何使用它?

    • 聚合管道是一系列的处理阶段(如$match$group$sort),用于对数据进行复杂处理。
    • 示例:
1
2
3
4
db.collection.aggregate([
{ $match: { status: "A" } },
{ $group: { _id: "$cust_id", total: { $sum: "$amount" } } },
]);
  1. 如何优化 MongoDB 查询性能?
    • 创建合适的索引,使用explain()分析查询计划。
    • 避免全集合扫描,减少返回的数据量。

索引与性能优化

  1. 什么是索引?如何在 MongoDB 中创建索引?
    • 索引是数据结构,用于加速查询。
    • 使用 createIndex() 创建索引,例如:
1
db.collection.createIndex({ field: 1 });
  1. 什么是复合索引?它的使用场景是什么?

    • 复合索引是包含多个字段的索引。
    • 适用于多字段组合查询的场景,例如 { field1: 1, field2: -1 }
  2. 如何监控和优化 MongoDB 的性能?

    • 使用 mongostatmongotopexplain("executionStats") 分析性能瓶颈。
    • 优化索引、分片和查询设计。

高可用性与集群管理

  1. 什么是复制集?它的作用是什么?

    • 复制集是一组维护相同数据集的 MongoDB 实例,提供高可用性数据冗余
    • 包括一个主节点和多个从节点。
  2. 什么是分片?它如何实现水平扩展?

    • 分片是将数据分布到多个服务器上的技术,用于处理大规模数据集。
    • 通过配置分片键实现数据分布。
  3. 如何管理 MongoDB 集群?

    • 使用 mongos 路由器管理分片集群,使用 mongod 管理复制集。
    • 监控集群状态,优化分片策略。

事务与一致性

  1. MongoDB 支持事务吗?如何使用?
    _ MongoDB 支持多文档事务,适用于需要原子性操作的场景。
    _ 示例:
1
2
3
session.startTransaction();
db.collection.insertOne({ field: "value" });
session.commitTransaction();
  1. 如何保证 MongoDB 中的数据一致性?
    • 对于复制集,使用"majority"读关注和写关注来确保强一致性。
    • 使用事务来保证多文档操作的一致性。

实践与工具

  1. 如何备份和恢复 MongoDB 数据?

    • 使用 mongodump 进行备份,mongorestore 进行恢复。
    • 也可以使用快照或云服务备份。
  2. 如何监控 MongoDB 的性能?

    • 使用 mongostatmongotop 实时监控数据库状态。
    • 集成 Prometheus 和 Grafana 进行长期性能分析。

go学习笔记

如何保证多个 goroutine 对共享变量的并发安全?

在 Go 语言中,多个 Goroutine 同时对共享变量进行读写时,可能会导致竞态条件(Race Condition),从而引发数据不一致的问题。为了保证并发安全,可以采用以下几种方法:

1. 使用互斥锁(sync.Mutex

  • 互斥锁用于确保同一时刻只有一个 Goroutine 可以访问共享变量。

  • 示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    package main

    import (
    "fmt"
    "sync"
    )

    var (
    counter int
    mutex sync.Mutex
    wg sync.WaitGroup
    )

    func increment() {
    mutex.Lock()
    counter++
    mutex.Unlock()
    wg.Done()
    }

    func main() {
    wg.Add(100)
    for i := 0; i < 100; i++ {
    go increment()
    }
    wg.Wait()
    fmt.Println("Counter:", counter) // 输出 100
    }

2. 使用读写锁(sync.RWMutex

  • 读写锁允许多个 Goroutine 同时读取共享变量,但写操作是独占的。
  • 示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package main

import (
"fmt"
"sync"
)

var (
counter int
rwMutex sync.RWMutex
wg sync.WaitGroup
)

func read() {
rwMutex.RLock()
fmt.Println("Counter:", counter)
rwMutex.RUnlock()
wg.Done()
}

func write() {
rwMutex.Lock()
counter++
rwMutex.Unlock()
wg.Done()
}

func main() {
wg.Add(200)
for i := 0; i < 100; i++ {
go write()
go read()
}
wg.Wait()
}

3. 使用原子操作(sync/atomic

  • 对于简单的数值类型,可以使用原子操作来保证并发安全。
  • 示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
    package main

import (
"fmt"
"sync"
"sync/atomic"
)

var (
counter int64
wg sync.WaitGroup
)

func increment() {
atomic.AddInt64(&counter, 1)
wg.Done()
}

func main() {
wg.Add(100)
for i := 0; i < 100; i++ {
go increment()
}
wg.Wait()
fmt.Println("Counter:", counter) // 输出 100
}

4. 使用通道(Channel)

  • 通过通道可以在 Goroutine 之间安全地传递数据,避免直接访问共享变量。
  • 示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
  package main

import (
"fmt"
"sync"
)

func main() {
var wg sync.WaitGroup
ch := make(chan int, 1) // 创建一个缓冲为1的通道
ch <- 0 // 初始化计数器

wg.Add(100)
for i := 0; i < 100; i++ {
go func() {
count := <-ch
count++
ch <- count
wg.Done()
}()
}
wg.Wait()
fmt.Println("Counter:", <-ch) // 输出 100
}

5. 使用sync.Map

  • sync.Map是并发安全的键值对集合,适合在读多写少的场景中使用。
  • 示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
  package main

import (
"fmt"
"sync"
)

func main() {
var m sync.Map
var wg sync.WaitGroup

wg.Add(100)
for i := 0; i < 100; i++ {
go func(i int) {
m.Store(i, i)
wg.Done()
}(i)
}
wg.Wait()

m.Range(func(k, v interface{}) bool {
fmt.Println("Key:", k, "Value:", v)
return true
})
}

总结

为了保证多个 Goroutine 对共享变量的并发安全,可以根据具体场景选择合适的方法:

  • 简单的数值操作可以使用原子操作。
  • 复杂的同步需求可以使用互斥锁或读写锁。
  • 避免直接共享变量时,可以使用通道或sync.Map
  • 选择合适的方法可以使代码更高效且更易维护。

channel 的底层实现原理是什么?

Go 语言中的 channel 是基于 CSP(Communicating Sequential Processes)模型实现的,用于在不同 goroutine 之间进行通信和同步。其底层实现原理主要包括以下几个关键点:

  1. 数据结构: Channel 在 Go 内部是由一个 hchan 结构体表示。该结构体包含了用于存储数据的环形缓冲区、用于同步的等待队列(sendq 和 recvq)以及用于保护数据结构的互斥锁(lock)。

  2. 缓冲区: Channel 可以是带缓冲区的或无缓冲区的。带缓冲区的 channel 在发送数据时,如果缓冲区未满,数据会被放入缓冲区,而不需要立即等待接收者。无缓冲区的 channel 则要求发送者和接收者必须同时准备好,否则会阻塞。

  3. 同步机制: 当缓冲区满或空时,发送或接收操作会阻塞,并将当前 goroutine 放入相应的等待队列(sendq 或 recvq)中。当条件满足时(如缓冲区有空闲空间或新数据到达),会唤醒等待的 goroutine。

  4. 关闭操作: 关闭 channel 会标记 channel 为关闭状态,并唤醒所有等待的 goroutine,发送操作会 panic,接收操作会返回零值和标记表示 channel 已关闭。

  5. 调度器集成: Go 的调度器会在 goroutine 阻塞或唤醒时进行调度,确保 goroutine 能够高效地在 channel 操作之间切换。

通过这些机制,Go 的 channel 实现了高效的 goroutine 间通信和同步。

GPM 调度模型

Go 语言的 GPM 模型是 Go 并发编程的核心,它由GoroutineProcessorMachine三部分组成,用于高效地管理和调度并发任务。以下是 GPM 模型的详细介绍:

1. Goroutine(G)

  • 轻量级的用户态线程,由 Go 运行时管理。
  • 相比操作系统线程,Goroutine 的创建和切换开销更小。
  • 通过go关键字启动,例如:go func() { ... }()

2. Processor(P)

  • 调度器执行的上下文,负责管理一组 Goroutine。
  • 每个 P 都有一个本地队列(Local Queue),用于存放等待执行的 Goroutine。
  • 默认情况下,Go 程序启动的 P 数量等于 CPU 核心数,可通过GOMAXPROCS调整。

3. Machine(M)

  • 操作系统线程,负责执行 Goroutine。
  • M 与 P 绑定,P 决定哪些 Goroutine 由 M 执行。
  • M 的数量通常略大于 P 的数量,以处理阻塞操作(如系统调用)。

GPM 模型的调度机制

  1. Goroutine 的创建

    • 当一个 Goroutine 被创建时,它会优先放入当前 P 的本地队列。
    • 如果本地队列已满,Goroutine 会被放入全局队列(Global Queue)。
  2. Goroutine 的执行

    • P 从本地队列中取出 Goroutine,并将其分配给 M 执行。
    • 如果本地队列为空,P 会尝试从全局队列或其他 P 的本地队列中窃取 Goroutine。
  3. 阻塞与解阻塞

    • 如果 Goroutine 执行阻塞操作(如系统调用),M 会释放 P 并与 Goroutine 一起进入阻塞状态。
    • 当 Goroutine 解阻塞后,M 会尝试绑定一个空闲的 P,如果没有空闲 P,Goroutine 会被放入全局队列。
  4. 调度器的主动调度

    • Go 调度器会在特定情况下主动调度 Goroutine,例如:
      • Goroutine 主动调用runtime.Gosched()
      • 系统监控线程(sysmon)发现长时间运行的 Goroutine。

GPM 模型的优势

  • 高效:避免了操作系统线程的频繁切换,降低了并发编程的开销。
  • 易用:开发者无需关心底层线程管理,只需使用go关键字启动 Goroutine。
  • 灵活:通过 P 的数量调整并发度,适应不同的硬件和任务需求。

总结

GPM 模型是 Go 语言高并发的基石,它通过高效的调度机制和轻量级的 Goroutine,使 Go 程序能够轻松处理成千上万的并发任务。理解 GPM 模型有助于更好地编写高性能的并发程序。

GC 垃圾回收算法

Go 语言的垃圾回收(GC)算法基于三色标记清除算法(Tri-color Mark-and-Sweep),并进行了优化以最大限度地减少停顿时间(Stop-the-World, STW)。以下是 Go 垃圾回收器的关键点和工作机制:

1. 三色标记清除算法

  • 三色标记:将对象分为三种颜色以跟踪其状态:
    • 白色:未访问可达的对象,表示为垃圾。
    • 灰色:已访问但尚未扫描的对象。
    • 黑色:已访问并扫描了其引用的对象。
  • 标记阶段
    • 从根对象开始,标记所有可达的对象(从白色变为灰色再到黑色)。
  • 清除阶段
    • 回收所有未标记白色的对象。

2. 并发垃圾回收

  • Go 的 GC 是并发的,标记阶段可与程序同时运行,从而减少 STW 时间。

3. 写屏障

  • 写屏障技术用于在垃圾回收期间追踪对象间的引用变化,确保在标记阶段标记新引用的对象。

4. 分代垃圾回收

  • Go 的垃圾回收器虽然不是传统意义上的分代收集器,但其设计自然优化了年轻代(短生命周期)对象的回收。

5. 触发条件

  • 堆增长:当堆内存增长到特定的阈值时,GC 会触发。
  • 手动触发:开发者可以通过runtime.GC()手动触发 GC。
  • 周期性检查:Go 运行时定期检查是否需要执行垃圾回收。

6. GC 调优

  • 使用GOGC环境变量可以调整垃圾回收器的行为(分别控制触发的频率和强度)。
  • 默认GOGC=100表示堆增长 100%就会再次触发 GC。

7. 监控 GC 性能

  • Go 提供了 API 来监控和获取 GC 性能指标,比如runtime.ReadMemStats
  • 这些指标包括 GC 次数、总耗时和堆内存使用情况。

示例:监控 GC 性能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package main

import (
"fmt"
"runtime"
"time"
)

func printGCStats() {
var m runtime.MemStats
runtime.ReadMemStats(&m)
fmt.Printf("GC Cycles: %d\n", m.NumGC)
fmt.Printf("GC Pause Total: %v\n", time.Duration(m.PauseTotalNs))
fmt.Printf("Heap Alloc: %v bytes\n", m.HeapAlloc)
}

func main() {
for i := 0; i < 10; i++ {
s := make([]byte, 1024*1024) // 分配1MB内存
_ = s
printGCStats()
time.Sleep(time.Second)
}
}

总结

Go 的垃圾回收器在设计上通过并行和并发的标记清除机制,以及写屏障技术,优化了垃圾回收过程以最大限度减少程序停顿。对短生命周期对象的有效回收帮助提高了内存管理效率。了解并合理配置 GC 参数,可以进一步优化 Go 程序的性能,特别是在高负载或实时性要求较高的应用中。

context 应用以及场景

在 Go 语言中,context包用于在不同 Goroutine 之间传递请求范围的信息、取消信号和截止日期。它是处理并发编程中请求上下文管理的重要工具。以下是关于 Go 的context及其应用场景的详细说明:

1. 基本概念

  • Context是 Go 语言中的一个接口,通常用于管理请求级别的状态,例如取消信号、截止日期和传递请求范围内的键值对。

2. Context 的类型

  • **context.Background()**:
    • 最顶层的上下文,一般用于主函数、初始化和测试代码的默认上下文,永远不会被取消且没有值和截止日期。
  • **context.TODO()**:
    • 当不确定要使用哪种 Context 或还没有数据时使用。即待决状态。
  • **context.WithCancel(parent)**:
    • 返回子ContextCancelFunc。调用CancelFunc会通知子 Context 取消。
  • **context.WithDeadline(parent, deadline)**:
    • 设置截止时间背景,超过这个时间后,Context 自动取消。
  • **context.WithTimeout(parent, timeout)**:
    • 类似WithDeadline,但更方便指定超时时间。
  • **context.WithValue(parent, key, val)**:
    • 生成一个带有键值对的子 Context,传递数据。

3. 应用场景

  • 取消信号传递
    • 在分布式系统中跨 API 边界传递取消通知,确保不再需要时终止请求。
  • 处理超时请求
    • 设置请求的截止时间,超时时自动取消请求以节省资源。
  • 跨 Goroutine 传递值
    • 保持请求链中上下文名称和数据的一致性,例如请求 ID、用户身份等。

4. Context 的使用示例

取消操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main

import (
"context"
"fmt"
"time"
)

func main() {
ctx, cancel := context.WithCancel(context.Background())
go func(ctx context.Context) {
select {
case <-ctx.Done():
fmt.Println("Operation canceled")
}
}(ctx)

time.Sleep(2 * time.Second)
cancel() // 触发取消操作
time.Sleep(1 * time.Second)
}

超时控制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main

import (
"context"
"fmt"
"time"
)

func main() {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()

select {
case <-time.After(3 * time.Second):
fmt.Println("Operation completed")
case <-ctx.Done():
fmt.Println("Operation timed out")
}
}

传递值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main

import (
"context"
"fmt"
)

func main() {
ctx := context.WithValue(context.Background(), "key", "value")

doSomething(ctx)
}

func doSomething(ctx context.Context) {
if value, ok := ctx.Value("key").(string); ok {
fmt.Println("Found value:", value)
} else {
fmt.Println("Key not found")
}
}

5. 注意事项

  • context应为请求域共享且易于取消,因此不建议将context作为结构体成员。
  • 应谨慎使用context.WithValue,避免过度使用而导致代码的可读性下降。

总结

context在 Go 的并发编程和请求处理中提供了强大的功能,尤其在处理取消信号、超时控制以及在链路中各服务间传递信息时。通过合理地应用context,可以编写出更清晰、更高效的 Go 程序。

内存对齐

Go 语言中的内存对齐是为了优化内存访问速度,以满足硬件架构对数据存储的要求。对齐可以提升内存访问效率,减少 CPU 访问内存的次数。以下是关于 Go 内存对齐的详细说明:

内存对齐的基本概念

  • 对齐原则:数据在内存中的地址应该是其所占字节数的倍数。例如,4 字节的int32应该存储在 4 的倍数的内存地址上。
  • 对齐的好处:对齐可提高数据访问效率,因为在许多 CPU 架构中,未对齐的内存访问可能会导致性能问题。

Go 中的对齐规则

  • Go 编译器会自动为变量分配内存并进行适当的对齐。
  • 各类型的内存对齐要求如下:
    • boolbyte:1 字节对齐。
    • int16uint16:2 字节对齐。
    • int32uint32float32:4 字节对齐。
    • int64uint64float64complex64:8 字节对齐。
    • 指针与平台相关,通常是 8 字节对齐(在 64 位系统上)。

结构体中的内存对齐

  • 结构体的内存布局依赖于它的字段顺序。编译器可能会在字段之间插入填充字节以保证对齐。
  • 结构体的总尺寸通常是最大对齐基数的整数倍。

示例:结构体内存对齐

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main

import (
"fmt"
"unsafe"
)
``

type StructExample struct {
a bool // 1 byte
b int32 // 4 bytes
c float64 // 8 bytes
}

func main() {
e := StructExample{}
fmt.Println("Size of StructExample:", unsafe.Sizeof(e)) //
}

在这个示例中,StructExample的内存总大小为 16 字节,其中包含了填充字节,以满足float64的对齐需求。

提高内存利用率的技巧

  • 优化字段顺序:通过调整结构体字段顺序,可以减少填充字节,优化内存使用。
  • 从大到小排序字段:按字段的大小和对齐需求从大到小排序,以减少对齐填充。

示例:优化后的结构体

1
2
3
4
5
6
7
8
9
10
type OptimizedStruct struct {
c float64 // 8 bytes
b int32 // 4 bytes
a bool // 1 byte
}

func main() {
e := OptimizedStruct{}
fmt.Println("Size of OptimizedStruct:", unsafe.Sizeof(e)) // 输出12字节
}

手动对齐和unsafe

  • Go 的unsafe包提供了一些工具来帮助开发者理解和控制内存对齐:
    • unsafe.Alignof:获取类型的对齐要求。
    • unsafe.Sizeof:获取类型的大小。
    • unsafe.Offsetof:获取结构体字段的偏移量。

总结

内存对齐影响到程序的性能和内存使用效率。Go 语言通过其编译器自动处理内存的对齐要求,但了解这些机制能够帮助开发者写出内存表现更高效的代码,并在需要时对结构体进行优化以减少不必要的内存开销。

sync.Pool

sync.Pool是 Go 标准库中的一种内存池机制,用于缓存和重用临时对象以减少内存分配和垃圾回收的负担。以下是关于sync.Pool的详细说明及其应用场景:

1. 基本概念

  • sync.Pool提供了一种临时对象的存储机制。
  • 被设计为减少需要频繁创建和销毁的对象的内存分配成本。
  • 适用于可以被重复使用但无需严苛管理生命周期的对象。

2. 工作原理

  • 对象获取:通过Get方法,sync.Pool尝试返回缓存池中的一个对象。如果池子为空,则调用用户定义的New函数创建一个新的对象。
  • 对象放回:通过Put方法,将对象放回池中以便重用。
  • 对象既可以由get分配,也可以主动放回池中,允许被未来的get线程使用。
  • sync.Pool中的对象在没有被其他引用持有时,可以在任意时刻被 GC 回收。

3. 特点

  • sync.Pool不保证在多核环境中所有的放入对象都能被未来的Get获取。
  • 池中的对象无活跃时,可能会被垃圾回收。

4. 应用场景

  • 临时对象缓存:适用于创建销毁开销较大的对象,例如缓冲区、网络连接、数据库条目等。
  • 降低 GC 压力:通过重用对象,降低垃圾回收压力及频率。

5. 使用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package main

import (
"fmt"
"sync"
)

func main() {
var pool = sync.Pool{
New: func() interface{} {
fmt.Println("Creating new instance")
return make([]byte, 1024) // 分配1KB的缓存
},
}

// Get an instance from the pool
instance := pool.Get().([]byte)
fmt.Printf("Got instance of size: %d\n", len(instance))

// Use the instance...

// Put the instance back into the pool
pool.Put(instance)

// Get another instance
anotherInstance := pool.Get().([]byte)
fmt.Printf("Got another instance of size: %d\n", len(anotherInstance))
}

输出:

1
2
3
Creating new instance
Got instance of size: 1024
Got another instance of size: 1024

6. 最佳实践

  • sync.Pool并非万能,不能用于需要严格管理的资源,特别是一些外部资源(如文件句柄、数据库连接)。
  • 应用于只有短生命周期的可回收的轻量对象。
  • 不要期望sync.Pool中的对象总是可以获取到,因为它们可能会被 GC。

总结

sync.Pool是一个简单且有效的工具,用于在多线程环境中使用并复用可缓存的对象。当适当使用时,它可以显著减少内存分配,提高性能。但因其设计为缓存而非长存储,使用时需要考虑对象容易被回收的特性

Worker Pool(工作池)

worker pool 模式是一种常见的并发编程技巧,用于限制并发 goroutine 的数量,从而提高系统性能和稳定性。它主要用于处理大量并发任务,避免因创建过多 goroutine 而导致资源耗尽。

工作原理

任务队列:

  • 将需要执行的任务放入一个任务队列(通常是 channel)。

Worker goroutine:

  • 创建一组 worker goroutine,它们从任务队列中获取任务并执行。
  • worker goroutine 的数量通常是固定的,以控制并发度。

任务分发: - 将任务放入任务队列,worker goroutine 会自动从队列中获取任务并执行。 -结果处理(可选): 可以使用 channel 或其他机制来收集 worker goroutine 的执行结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
package main

import (
"fmt"
"sync"
"time"
)

type Task struct {
ID int
}

func worker(id int, tasks <-chan Task, wg *sync.WaitGroup) {
defer wg.Done()
for task := range tasks {
fmt.Printf("Worker %d processing task %d\n", id, task.ID)
time.Sleep(1 * time.Second) // 模拟任务执行
}
}

func main() {
numWorkers := 3
numTasks := 10

tasks := make(chan Task, numTasks)
var wg sync.WaitGroup

// 创建 worker goroutine
for i := 1; i <= numWorkers; i++ {
wg.Add(1)
go worker(i, tasks, &wg)
}

// 分发任务
for i := 1; i <= numTasks; i++ {
tasks <- Task{ID: i}
}
close(tasks) // 关闭任务队列

// 等待所有 worker goroutine 完成
wg.Wait()
fmt.Println("All tasks processed")
}

参考

digitalOcean: concurrency in go

ElasticSearch学习笔记

ElasticSearch

Elasticsearch 的工作原理。

Elasticsearch 是一个分布式的搜索和分析引擎,常用于大规模数据的全文本搜索、日志分析和实时数据流分析等场景。它基于 Apache Lucene 库构建,提供了一个强大、便捷的 RESTful API 来进行索引和搜索数据。

Elasticsearch 的工作原理包括以下几个关键点:

  1. 索引和文档

    • 数据在 Elasticsearch 中被组织为一个或多个索引(Index),每个索引包含若干文档(Document)。每个文档是 JSON 格式的,包含了一系列属性(fields),类似于数据库中的行。
  2. 分片和副本

    • 为了可伸缩性和高可用性,每个索引可以分为多个分片(Shard),默认情况下有五个主分片。每个主分片可以有一个或多个副本(Replica),以确保数据的冗余和安全。
  3. 聚合

    • Elasticsearch 不仅支持全文检索,还支持通过聚合(Aggregation)对数据进行复杂的分析和统计。
  4. 分布式架构

    • Elasticsearch 的集群由一个或多个节点组成,每个节点是一个运行 Elasticsearch 的实例。分布式架构使其可以轻松扩展,以处理大量的数据和请求。
  5. 全文搜索

    • Elasticsearch 能够通过倒排索引进行快速的全文搜索,这使其在处理文本数据时非常高效。

总之,Elasticsearch 通过其分布式索引和搜索能力,为用户提供了一个灵活和可伸缩的解决方案,用于处理和分析大量数据。

什么是节点、集群、索引、类型、文件夹和分片?

在 Elasticsearch 的架构中,以下是几个重要概念的定义:

  1. 节点(Node)

    • 节点是一个运行中的 Elasticsearch 实例。一个节点在集群中承担数据存储和搜索功能。多个节点可以组成一个集群。
  2. 集群(Cluster)

    • 集群是由一个或多个节点组成的集合,它们协同工作来存储和检索数据。集群由一个唯一的名称标识,所有属于同一集群的节点必须有相同的集群名称。
  3. 索引(Index)

    • 索引用于存储相关文档的集合,类似数据库的概念。在一个索引中,数据通过 JSON 格式的文档形式存在。
  4. 类型(Type)

    • 在早期版本的 Elasticsearch 中,类型用于定义索引中的文档分类。不过从 Elasticsearch 7 开始,每个索引只支持一个类型,类型的概念逐渐被淡化和放弃。
  5. 分片(Shard)

    • 分片是一个索引的逻辑分区。每个索引可以配置多个分片。分片的设计使得 Elasticsearch 能够水平扩展,允许保存并分布大量数据。
  6. 副本分片(Replica Shard)

    • 副本分片是主分片的副本,用于提高数据的高可用性和读性能。Elasticsearch 会在不同的节点上分配主分片和副本分片,以实现冗余。
  7. 文档(Document)

    • 文档是存储在索引中的基本信息单元,类似于关系型数据库中的行。每个文档都是以 JSON 格式存储的。

这些概念共同组成了 Elasticsearch 的基本架构,使其可以有效地存储、搜索和分析大规模数据。

如何定义和创建索引?

在 Elasticsearch 中,创建索引的过程通常包括定义索引的映射(mapping)和设定索引的配置,如分片和副本的数量。以下是步骤:

  1. 定义索引映射(Mapping)

    • 索引映射定义了索引中文档的结构,包括字段的类型。例如,字段可以是数字、字符串、日期等。
  2. 创建索引

    • 你可以通过 Elasticsearch 的 RESTful API 创建索引。使用 HTTP 请求,以 JSON 格式指定索引的设置和映射。

示例创建索引的 API 请求:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
PUT /my_index
{
"settings": {
"number_of_shards": 3,
"number_of_replicas": 2
},
"mappings": {
"properties": {
"field1": { "type": "text" },
"field2": { "type": "keyword" },
"field3": { "type": "date" },
"field4": { "type": "integer" }
}
}
}

在这个例子中,创建了一个名为my_index的索引:

  • settings:定义了索引的配置,包括分片数量(number_of_shards)和副本数量(number_of_replicas)。
  • mappings:指定了文档中字段的数据类型,如field1是文本类型,field2是关键字类型,field3是日期类型,而field4是整数类型。

你可以根据需求调整这些设置和映射,以适应你的数据模型和查询性能要求。

什么是分片?为什么需要分片?

分片(Shard)是 Elasticsearch 中一种重要的概念,它将索引划分为若干更小的部分。这种分区可以提高数据的管理和检索能力。以下是关于分片的详细介绍:

  1. 什么是分片

    • 分片是一个独立的搜索引擎实例。每个分片都包含索引的一部分数据,并能独立执行 CRUD(创建、读取、更新、删除)和搜索操作。
    • 每个索引可以配置一个或多个主分片(Primary Shard),这些分片共同存储索引中的所有数据。
  2. 需要分片的原因

    • 可扩展性:单个节点通常无法存储或高效管理海量数据。通过将索引分为多个分片,可以将数据和负载分布到集群中的多个节点上,从而实现水平扩展。
    • 并行处理:多个分片可以分布在多个节点上,这意味着 Elasticsearch 可以并行进行数据操作,提高处理速度。
    • 高可用性:通过创建分片的副本(Replica Shards),可以在集群节点出现故障时提供数据冗余和高可用性。
  3. 分片的副本

    • 每个主分片可以有一个或多个副本分片(Replica Shard)。副本分片不仅用于灾难恢复,也可以用于分担读操作的负载,提高查询效率。

通过分片机制,Elasticsearch 能够处理大规模数据集,同时提供高性能和高可用性,使其表现出色,特别是在需要快速响应和处理大量数据的应用场景中。

分片和副本之间的区别是什么?

片(Shard)和副本(Replica)是 Elasticsearch 中两个密切相关但有不同功能的概念。

  1. 分片(Primary Shard)

    • 作用:每个索引的数据都被划分为若干分片,每个分片是完整索引的数据的一个子集。分片的主要作用是实现数据分发和并行计算,从而提高 Elasticsearch 的吞吐量和性能。
    • 数量:每个索引的主分片数量是在索引创建时设定的,并且一旦索引创建就不能更改。
  2. 副本(Replica Shard)

    • 作用:副本是主分片的完整副本,主要用于提供冗余,以实现高可用性和故障恢复。此外,副本帮助分担查询负载,以提高读取性能。
    • 数量:每个主分片可以有一个或多个副本。副本的数量可以动态调整。

区别

  • 目的:主分片的目的是对数据进行分区以实现扩展性;副本的目的是提供冗余和提高读取的容错能力。
  • 数据写入:数据只能写入主分片,而副本接收来自主分片的数据同步。
  • 容灾恢复:当主分片故障时,集群可以选举一个副本作为新的主分片以保证数据的可用性。

这种分片和副本的设计,使 Elasticsearch 能够高效地处理大规模的读取和写入请求,同时保证数据不丢失。

如何配置分片数量和副本数量?

配置分片数量和副本数量是在 Elasticsearch 中创建索引时非常重要的步骤。以下是如何进行这些配置的方法:

  1. 分片数量(number_of_shards)

    • 分片数量决定了索引将如何分布在集群的多个节点上。配置时要考虑数据量和节点数量。
    • 分片数量在索引创建的时候设置,之后无法更改。因此,必须提前根据预期数据量和集群规模来规划。
  2. 副本数量(number_of_replicas)

    • 副本数量指的是每个主分片的副本数量。副本可以动态调整。
    • 增加副本数量有助于提高系统的读取性能和提供数据的冗余性。

配置示例

当你创建一个新的索引时,可以通过如下的 HTTP 请求设置分片和副本的数量:

1
2
3
4
5
6
7
PUT /my_index
{
"settings": {
"number_of_shards": 5,
"number_of_replicas": 1
}
}

在这个示例中:

  • number_of_shards被设置为5,表示该索引将拥有 5 个主分片。
  • number_of_replicas被设置为1,表示每个主分片将有一个副本。

注意事项

  • 分片数量应考虑集群的硬件配置以及预期的数据增长。
  • 副本提供高可用性,因此至少要有一个副本来确保系统能够在单节点故障时保持可用性。
  • 可以在运行时动态调整副本数量,但分片数量必须在索引创建时确定并且不能以后更改。

在使用过程中如何动态调整分片/副本?

在 Elasticsearch 中,虽然分片数量一旦指定就不能修改,但可以动态调整副本(replica)的数量。这是如何进行调整的步骤:

  1. 动态调整副本数量
    • 调整副本数量的目的是增强读取性能和提高数据的冗余性。你可以根据系统负载或可用性需求来增减副本数量。

调整副本的 API 请求示例

可以通过以下 API 请求动态调整某个索引的副本数量:

1
2
3
4
PUT /my_index/_settings
{
"number_of_replicas": 2
}

在这个例子中,我们把索引my_index的副本数量设置为2

注意事项

  • 副本数量可以动态增加或减少,但每个操作可能会导致数据重分配,消耗系统资源。
  • 如果你减少副本,可能会在数据节点故障时降低数据的可用性。
  • 增加太多副本可能导致资源浪费,因为每个副本都需要存储和维持完整的数据集。

通过这种设置,虽然不能直接调整分片数量,但副本的调整给了很大的灵活性来优化 Elasticsearch 集群的性能和可用性。

解释一下匹配查询与术语查询的区别。

在 Elasticsearch 中,匹配查询(match查询)和术语查询(term查询)是两种最常用的查询类型,它们用于不同的目的,处理查询的方式也有所不同:

  1. 匹配查询(Match Query)
    • 用途: 用于全文搜索。适合在分词的字段上进行搜索,比如文本字段。
    • 行为: match查询会对搜索词进行分词,然后在倒排索引中查找这些词的匹配项。
    • 场景: 如果你在搜索类似于一段文本而非精确数值或关键词时,这种查询最为合适。
    • 示例:
1
2
3
4
5
6
7
{
"query": {
"match": {
"field_name": "search term"
}
}
}
  1. 术语查询(Term Query)
    • 用途: 用于精确匹配,适合未分词的字段,如关键词、ID、标签、数值字段。
    • 行为: term查询会直接在索引中查找完全匹配的词项。不进行分词操作。
    • 场景: 适用于精确搜索(如搜索一个特定的用户 ID),而非文本分析的场合。
    • 示例:
      1
      2
      3
      4
      5
      6
      7
      {
      "query": {
      "term": {
      "field_name": "exactTerm"
      }
      }
      }

区别总结:

  • match查询适合应用于需要文本分析(即需要分词)的场景,而term查询则用于精确值匹配,不适合分词后搜索。
  • 使用match查询时,Elasticsearch 会根据字段的映射类型和相关分析器进行分词等预处理,而term查询直接查找精确匹配的值。

选择合适的查询类型有助于获得更准确的搜索结果,优化系统性能。

如何优化查询性能?

优化 Elasticsearch 查询性能可以提高数据检索速度并降低资源消耗。以下是一些关键的优化策略:

  1. 使用适当的查询类型

    • 根据需求选择合适的查询类型。对于全文搜索,使用match查询,而对于精确匹配,使用term查询。
  2. 减少数据传输

    • 选择合适的字段并仅检索需要的字段。使用_source参数仅返回需要的字段数据。
  3. 过滤与查询分离

    • 利用filter上下文来替代query上下文进行无需评分的过滤操作,这样 Elasticsearch 可以利用缓存提高性能。
  4. 合理配置索引与映射

    • 为常用搜索字段设置合适的分词器,避免不必要的索引字段。
    • 使用keyword类型字段存储需要做精确匹配的文本。
  5. 利用缓存

    • 对频繁执行的静态查询,利用 Elasticsearch 的查询缓存机制。
  6. 调整分片和副本

    • 通过配置适当的分片和副本数量,确保查询负载在集群中均匀分布。
  7. 数据归档和多索引管理

    • 对于历史数据,考虑使用不同的索引或归档方案,以减少热数据集的大小。
  8. 硬件和集群优化

    • 确保有足够的内存、CPU 和磁盘 I/O,以支持 Elasticsearch 的需求。
  9. 使用聚合适当

    • 在使用聚合(aggregations)时,尽量限定其作用范围以有限制结果集的规模。

这些优化策略需要根据特定应用场景来灵活运用,以在不增加不必要的资源消耗的前提下获得最佳性能。定期监控系统的性能表现和瓶颈,也是保持系统高效运作的重要手段。

如何进行索引的备份和恢复?

Elasticsearch 提供了快照和恢复的功能以备份和恢复索引数据。这种机制通过一个称为”快照/恢复(snapshot/restore)”的 API 来管理。以下是如何进行索引备份和恢复的步骤:

索引备份

  1. 设置存储库
    • 首先需要创建一个用于存储快照的存储库(repository)。存储库可以是共享文件系统、Amazon S3、HDFS 等。
    • 创建文件系统类型存储库的示例:
      1
      2
      3
      4
      5
      6
      7
          PUT _snapshot/my_backup
      {
      "type": "fs",
      "settings": {
      "location": "/mount/backups/my_backup"
      }
      }
  2. 创建快照
    • 使用快照 API 创建快照。可以指定单个索引或者所有索引。
    • 创建快照的示例:
      1
      2
      3
      4
      5
      6
         PUT _snapshot/my_backup/snapshot_1
      {
      "indices": "index_1,index_2",
      "ignore_unavailable": true,
      "include_global_state": false
      }

索引恢复

  1. 从快照中恢复
    • 使用恢复 API 从快照中恢复索引。可以选择恢复到不同的集群或同一集群。
    • 恢复索引的示例:
      1
      2
      3
      4
      5
      6
         POST _snapshot/my_backup/snapshot_1/_restore
      {
      "indices": "index_1",
      "ignore_unavailable": true,
      "include_global_state": false
      }

在开始备份和恢复前,确保:

  • 所有节点可以访问快照存储库。
  • 提前评估恢复操作对性能的影响,特别是在繁忙的集群中。
  • 快照过程一般不会影响集群的正常运行,但仍建议在低流量时段使用。

通过快照和恢复机制,可以有效备份和恢复 Elasticsearch 的数据,确保数据安全和快速恢复。

如何处理索引的热/冷数据管理?

在 Elasticsearch 中,处理索引的热/冷数据管理是一种优化存储和查询性能的策略。热/冷数据管理将活跃数据(热数据)和非活跃数据(冷数据)区分开来,并且分别存储和处理,以减少资源消耗并优化查找性能。

热/冷数据管理策略

  1. 定义热和冷数据

    • 热数据是近期活跃使用或查询频繁的数据。通常存储在高性能硬件(如 SSD)上以提高查询性能。
    • 冷数据是历史性、查询频率低的数据。通常可以存储在大容量但低成本的存储上(如 HDD)。
  2. 使用索引生命周期管理(ILM)

    • Elasticsearch 提供了索引生命周期管理(ILM)功能,可以根据数据的生命周期阶段自动迁移索引数据。
    • 配置 ILM 策略时,你可以指定索引生命周期阶段,如热、温、冷、删除。
  3. 创建 ILM 策略

    • 以下是一个简单 ILM 策略的示例,它定义了数据从热到冷的迁移:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
         PUT _ilm/policy/my_policy
      {
      "policy": {
      "phases": {
      "hot": {
      "actions": {
      "rollover": {
      "max_age": "30d",
      "max_size": "50gb"
      }
      }
      },
      "warm": {
      "min_age": "30d",
      "actions": {
      "allocate": {
      "number_of_replicas": 1
      }
      }
      },
      "cold": {
      "min_age": "60d",
      "actions": {
      "allocate": {
      "require": {
      "data": "cold"
      }
      },
      "freeze": {}
      }
      },
      "delete": {
      "min_age": "90d",
      "actions": {
      "delete": {}
      }
      }
      }
      }
      }
  4. 标签和节点分配

    • 使用标签或者自定义属性标识不同类型的数据节点(热节点、冷节点),然后通过 ILM 策略将索引分配到这些节点。
  5. 数据迁移

    • 根据需要,手动或者自动化地将索引从热节点迁移到冷节点以进行节约和性能优化。

通过适当的热/冷数据管理策略,Elasticsearch 可以更有效地利用系统资源,减少热数据集的压力,同时降低存储成本。

如何监控集群健康状况?

监控 Elasticsearch 集群的健康状况对于确保其稳定性和性能至关重要。以下是一些常用的方法和工具,用于监控 Elasticsearch 集群的健康状况:

  1. 集群健康 API
    • 使用_cluster/health API 来快速检查集群的总体健康状态。
    • 返回的信息包括集群的状态(绿色、黄色、红色)、节点数量、正在进行中的分片重新分配等。
      bash GET _cluster/health
  • 绿色状态表示所有的分片都正常,黄色状态表示存在具有主分片的索引但是缺乏其副本,红色状态表示存在无法访问的主分片。
  1. 集群状态 API

    • 使用_cluster/stats API 获取集群的详细统计信息,包括文档数量、存储使用情况以及各个节点的详细信息。
      bash GET _cluster/stats
  2. 节点统计 API

    • 节点层面的统计信息可以通过_nodes/stats API 获取,包含信息如 CPU 使用率、内存使用、文件系统、线程池等,帮助识别节点级别的问题。
      bash GET _nodes/stats

如何优化 Elasticsearch 的性能?

优化 Elasticsearch 的性能需要综合考虑索引设计、集群配置和查询效率。以下是一些关键的优化策略:

索引设计优化

  1. 合理选择字段类型

    • 根据数据类型和使用方式选择合适的字段类型,避免不必要的分析。对于精确匹配,使用keyword类型,而非全文分析使用text类型。
  2. 字段映射优化

    • 禁用或者限制不必要的字段索引。对于不需要搜索的字段,设置index: false
  3. 分片调整

    • 根据数据量和查询需要,适当设置索引的分片数量。过多或过少的分片都会影响性能。一般的经验法则是每个分片大小在 10-50GB 之间。

查询优化

  1. 使用过滤

    • 对于不需要评分的条件,使用filter而不是query来提高查询速度,并利用缓存机制。
  2. 限制返回字段

    • 只请求必要的字段,使用_source过滤避免不必要的数据传输。
  3. 异步和批量处理

    • 尽量使用批量处理(例如使用批量 API)来减少请求数量。

系统和集群配置

  1. 硬件配置

    • 确保内存(heap size)和 CPU 等资源充裕。通常分配给 Elasticsearch 进程的 JVM 堆内存不要超过总内存的 50%。
  2. 异步刷新和合并

    • 调整刷新和合并频率。对于写入密集的场景,稍微延迟刷新时间可以减少 I/O 负担。
  3. 节点角色优化

    • 将不同工作类型分配到不同节点角色如数据节点、主节点和协调节点以优化负载。

日常运维和监控

  1. 定期监控和分析

    • 使用 Elasticsearch 提供的监控工具(例如 Kibana Monitoring)定期监控集群性能,调整策略。
  2. 索引生命周期管理(ILM)

    • 使用 ILM 策略自动管理索引的创建、关闭、删除以及冷热数据的定期自动迁移。

通过结合以上策略,能有效提升 Elasticsearch 的查询速度和数据处理效率,同时确保集群的稳定运行。定期分析和调优同样重要,以便及时响应不断变化的负载和需求。

哪些因素会影响 Elasticsearch 性能?

Elasticsearch 的性能受多种因素影响,主要包括硬件配置、索引设计、集群设置以及查询模式。以下是一些关键因素:

  1. 硬件配置

    • CPU:CPU 的核心数量和处理能力直接影响查询和索引的速度。
    • 内存:充足的 RAM 能够提升缓存命中率,减少磁盘 I/O,尤其是对于大数据集。JVM 半数以上的内存通常被分配给堆内存。
    • 磁盘 I/O:SSD 比 HDD 提供更快的读写速度,这对高频查询和索引任务尤其重要。
  2. 索引设计

    • 分片数量:不适当的主分片和副本分片数量会导致内存浪费或 I/O 瓶颈。
    • 字段映射:复杂或不必要的字段映射会增加索引和查询的复杂性。
    • 文档结构:扁平化的文档结构通常比深层嵌套结构性能更好。
  3. 集群设置

    • 节点配置:角色分配不合理(如所有节点都为主节点或数据节点)会导致性能瓶颈。
    • 网络延迟:高网络延迟会影响数据节点间的通信和协调节点的响应速度。
  4. 查询模式

    • 搜索量和频率:高频繁复杂的查询会增加系统负载,影响响应时间。
    • 过滤 vs 查询:使用query而不是filter会导致不必要的评分计算。
  5. 索引操作

    • 刷新和合并频率:过于频繁的刷新操作可能导致 CPU 和 I/O 资源浪费。
    • 更新和删除操作:这些操作需要额外的系统资源进行重建和合并。
  6. 系统初始化和垃圾回收

    • 启动时的预热和数据加载以及不当的 GC(垃圾回收)配置会影响系统的响应时间。
  7. 数据大小和分布

    • 数据集的大小及其在索引中的分布方式会影响查找性能。

注意这些因素对于不同的使用场景可以有不同的影响,需要根据具体情况进行诊断和优化。定期的监控和调整是确保 Elasticsearch 性能的关键。

解释什么是集群分片再平衡。

集群分片再平衡(Shard Rebalancing)是 Elasticsearch 集群管理中的一个关键功能,旨在保持分片在集群中的均匀分布,从而优化资源利用和提高性能。

分片再平衡的概念

  1. 目的

    • 确保分片在集群中的节点上均匀分布,以避免单个节点成为瓶颈。
    • 提高故障容错能力,确保副本分片不与其对应的主分片部署在同一个节点上。
  2. 触发条件

    • 新节点加入集群:当新的数据节点被添加到集群后,Elasticsearch 会自动进行分片再平衡,将数据分片移动到新节点上以均衡负载。
    • 节点离开或故障:系统检测到数据节点离开或故障,会将其上的分片重新分配到其他节点。
    • 手动触发:管理员可以通过 API 手动触发再平衡,调整特定索引或节点的分片配置。
  3. 配置和控制

    • 可以通过集群设置控制分片再平衡的行为,例如设置集群的再平衡权重、延迟等。
    • 使用 API 限制或调整再平衡的操作以避免影响集群性能,例如在高负载期间抑制再平衡。
  4. 优先级

    • Elasticsearch 优先恢复数据的高可用性(如主分片恢复)然后才进行性能优化再平衡。

再平衡过程

  • Elasticsearch 会自动计算和重新分配需要移动的分片。过程中会尽量减少对集群性能的影响。
  • 再平衡的具体实施与各个分片的分配策略有关,通常根据节点负载和分片大小优先进行调整。

分片再平衡使得 Elasticsearch 能够动态响应集群配置的变化,确保持续的高性能和数据的高可用性。这一过程在维护中普遍自动进行,也可以通过配置进行微调。

如何使用 Elasticsearch 进行全文检索?

使用 Elasticsearch 进行全文检索是其核心功能之一,下面是实现全文检索的步骤和要点:

配置索引

  1. 定义映射
    • 在创建索引时,指定字段的typetext以支持全文分析和搜索。

PUT /my_index

1
2
3
4
5
6
7
8
9
{
"mappings": {
"properties": {
"content": {
"type": "text"
}
}
}
}
  1. 分析器(Analyzers)
  • Elasticsearch 使用分析器将文本分解为词元。默认使用标准分析器,也可以根据语言或需求选择其他分析器(如englishcustom等)。

索引数据

  • 将文档索引到 Elasticsearch:

POST /my_index/_doc

1
2
3
{
"content": "Elasticsearch 是一个强大的搜索工具。"
}

搜索查询

  1. 使用match查询
    • match查询用于执行全文检索,会经过分析器处理从而支持复杂的自然语言搜索。

GET /my_index/_search

1
2
3
4
5
6
7
{
"query": {
"match": {
"content": "搜索工具"
}
}
}
  1. 布尔查询(Boolean Query)
  • 可以使用bool组合多个match查询以实现更复杂的检索条件。

GET /my_index/_search

1
2
3
4
5
6
7
8
9
10
{
"query": {
"bool": {
"must": [
{ "match": { "content": "Elasticsearch" } },
{ "match": { "content": "工具" } }
]
}
}
}

结果排序

  • 搜索结果按相关性得分排序,得分由 Lucene 评分机制计算。
  • 可以通过调整boost参数来影响特定字段的权重,从而改变排序。

高亮显示

  • 使用highlight参数在返回结果中突出显示匹配的文本部分:

GET /my_index/_search

1
2
3
4
5
6
7
8
9
10
11
12
{
"query": {
"match": {
"content": "Elasticsearch"
}
},
"highlight": {
"fields": {
"content": {}
}
}
}

以上流程是使用 Elasticsearch 进行全文检索的基本步骤,通过配置适当的映射、索引数据和查询,相结合分析器,还可以进一步优化性能以满足更复杂的搜索需求。

点穴更新(Point-in-time)和搜索后分页(Search After)是什么?

点穴更新(Point-in-time)和搜索后分页(Search After)是 Elasticsearch 中用于处理大数据集搜索和分页的机制。

点穴更新 (Point-in-time, PIT)

  • 概念:点穴更新是一种在指定时间点创建数据快照的技术,允许一致地读取数据,即使数据在查询时发生变化。
  • 用途:PIT 特别适合长时间运行的分页操作、深度分页、快照备份等使用场景。
  • 优势:通过 PIT 可以在不断变化的索引上保持一致的视图,从而避免在分页过程中数据偏移或丢失。
  • 实现:在进行分页查询之前,通过建立 PIT 获取当前数据快照。

示例创建 PIT:POST /my_index/_pit?keep_alive=1m

搜索后分页 (Search After)

  • 概念:搜索后分页是一种高效的分页方式,适合需要从大数据集的某一位置开始分块读取数据的操作。
  • 用途:适用于深度分页,取代传统分页(即从特定“页”开始检索数据页)的高计算代价。
  • 优势:无需计算前面的记录总数,只需根据当前排序结果的最后一个记录开始检索,因此性能更高。
  • 实现:利用结果中的排序属性,将search_after参数设置为上一个页的最后一个文档的排序值。

示例使用search_after
GET /_search

1
2
3
4
5
6
7
8
{
"size": 10,
"query": {
"match_all": {}
},
"sort": [{ "timestamp": "asc" }],
"search_after": [1620000000000]
}

结合点穴更新和搜索后分页,可以在处理大数据集时获得一致且高效的查询结果,特别是在需要分页的大型日志或事件数据集中

倒排索引

倒排索引(Inverted Index)是 Elasticsearch 等搜索引擎的核心数据结构,用于快速实现全文搜索。与传统的正排索引不同,倒排索引将文档中的内容映射到包含该内容的文档,而不是文档到内容的映射。

倒排索引的工作原理

  1. 文档分词:将文档内容通过分词器(Tokenizer)分解成一个个词项(Term)。
  2. 建立词项到文档的映射:记录每个词项出现在哪些文档中,并保存词项的位置、频率等信息。
  3. 查询优化:通过倒排索引快速找到包含特定词项的文档,而不需要扫描所有文档。

示例

假设有以下文档:

  • 文档 1:Elasticsearch is fast
  • 文档 2:Elasticsearch is powerful
  • 文档 3:Lucene is the core

倒排索引将构建如下:

1
2
3
4
5
6
7
"elasticsearch" -> [文档1, 文档2]
"is" -> [文档1, 文档2, 文档3]
"fast" -> [文档1]
"powerful" -> [文档2]
"lucene" -> [文档3]
"the" -> [文档3]
"core" -> [文档3]

倒排索引的优势

  • 高效查询:通过索引词项直接定位文档,大大减少查询时间。
  • 支持复杂查询:支持布尔查询、短语查询、模糊查询等高级搜索功能。
  • 灵活分析:支持分词器、同义词、停用词等文本分析功能。

倒排索引的组成部分

  • 词典(Term Dictionary):存储所有唯一的词项。
  • 倒排表(Postings List):记录每个词项对应的文档列表及词频、位置等信息。

在 Elasticsearch 中的应用

  • Elasticsearch 为每个字段(如text类型的字段)创建一个倒排索引。
  • 倒排索引存储在 Lucene 中,由 Elasticsearch 分布式管理。

倒排索引是全文搜索的核心,是实现高效检索的关键技术。理解其工作原理有助于更好地优化搜索性能。

mapping 设计技巧

分片分配策略

Elasticsearch 的分片分配策略是确保集群性能、数据冗余和负载均衡的核心机制。以下是分片分配策略的详细说明和优化建议:

1. 分片分配基础

  • 主分片(Primary Shard):存储数据的主力分片,每个文档会被分配到一个主分片中。
  • 副本分片(Replica Shard):主分片的副本,用于提高数据冗余和查询性能。

2. 分片分配策略

  • 负载均衡:Elasticsearch 会自动将分片均匀分布在集群中的节点上,以避免单个节点负载过高。
  • 故障恢复:如果某个节点宕机,其分片会重新分配到其他可用节点上。
  • 机架感知(Rack Awareness):通过配置cluster.routing.allocation.awareness.attributes,确保分片及其副本分布在不同的物理机架或可用区,提高容错能力。

3. 分片分配设置

  • 节点属性分配:使用index.routing.allocation.includeindex.routing.allocation.excludeindex.routing.allocation.require来控制分片分配到特定节点。例如:

PUT /my_index/_settings

1
2
3
{
"index.routing.allocation.include.zone": "us-east1"
}
  • 分片过滤:通过节点标签或自定义属性,限制分片只能分配到特定节点。

4. 动态分片管理

  • 手动分配:使用_cluster/reroute API 手动移动分片,例如:
    POST /_cluster/reroute

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    {
    "commands": [
    {
    "move": {
    "index": "my_index",
    "shard": 0,
    "from_node": "node1",
    "to_node": "node2"
    }
    }
    ]
    }
  • 分片恢复:通过cluster.routing.allocation.node_initial_primaries_recoveriescluster.routing.allocation.node_concurrent_recoveries控制分片恢复的并发数。

5. 分片分配优化

  • 分片大小控制:每个分片的大小建议控制在 10GB 到 50GB 之间,避免分片过大或过小。
  • 分片数量优化:根据数据量和查询负载合理设置分片数量。过多的分片会增加集群管理开销,过少的分片可能导致查询性能下降。
  • 分片均衡:可以使用_cluster/reroute API 或_cluster/settings API 动态调整分片分配策略。

6. 避免热点问题

  • 路由优化:使用自定义路由将特定数据分配到固定分片,避免热点分片。
  • 查询负载均衡:确保查询请求均匀分布在所有分片上。

7. 监控与调整

  • 使用_cat/shards API 监控分片分配状态。
  • 定期检查集群健康和分片分布情况,优化分片分配策略。

通过合理设计分片分配策略,可以显著提高 Elasticsearch 集群的性能、可用性和可扩展性。务必结合业务需求和硬件环境进行优化。

深分页问题

使用 scroll API 对大数据量查询

  • 会生成数据快照,适合非实时的大批量数据导出
  • 需要管理 scroll_id 的生命周期

scroll API 生命周期的关键步骤:

存活时间控制

  • 在初始请求中通过 ?scroll=1m 参数设置上下文存活时间(示例使用 1 分钟)
  • 每次续期时会重置倒计时(最后一次请求后开始倒计时)

显式删除 scroll_id

1
2
3
4
5
6
7
8
# 单个删除
curl -X DELETE "{{ES_HOST}}/_search/scroll" -H 'Content-Type: application/json' -d'
{
"scroll_id" : "DXF1ZXJ5QW5kRmV0Y2gBAAAAAAAAAD4WYm9laVY..."
}'

# 批量删除所有历史
curl -X DELETE "{{ES_HOST}}/_search/scroll/_all"

存活时间推荐参数

  • 大数据量(百万级):设置 10-30 分钟
  • 常规场景:控制在 1-5 分钟内
  • 配合管道处理时:设置略长于预估处理时间

监控方法

1
2
# 查看当前活跃的 scroll 上下文
curl -X GET "{{ES_HOST}}/_nodes/stats/indices/search?pretty"

refresh vs flush

如何使用 PIT(Point-in-Time)+ search_after 优化分页查询

使用 PIT(Point-in-Time)配合 search_after 实现安全分页的步骤:

  1. 创建 PIT(有效期为 5 分钟)
1
2
3
curl -X POST "{{ES_HOST}}/index/_pit?keep_alive=5m"
# 返回结果示例:
{ "id": "48gkwmdlKG..." } # 保存这个 pit_id
  1. 首次查询
1
2
3
4
5
6
7
8
9
10
11
12
13
curl -X GET "{{ES_HOST}}/_search" -H 'Content-Type: application/json' -d'
{
"size": 100,
"pit": {
"id": "{{PIT_ID}}",
"keep_alive": "5m"
},
"sort": [
{ "timestamp": "desc" },
{ "_id": "asc" }
]
}'
# 搜索结果中会返回新的 pit_id 用于后续查询
  1. 后续分页(使用 search_after)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
curl -X GET "{{ES_HOST}}/_search" -H 'Content-Type: application/json' -d'
{
"size": 100,
"pit": {
"id": "{{NEW_PIT_ID}}",
"keep_alive": "5m"
},
"sort": [
{ "timestamp": "desc" }, # 排序字段必须与首次查询一致
{ "_id": "asc" }
],
"search_after": [
"2025-02-24T12:34:56", # 使用上一次结果最后的排序值
"document_id_123"
]
}'
  1. 显式关闭 PIT(立即释放资源)
1
2
3
4
curl -X DELETE "{{ES_HOST}}/_pit" -H 'Content-Type: application/json' -d'
{
"id" : "{{PIT_ID}}"
}'

工作特点:

  • PIT 会在 keep_alive 时间后自动清理(无需强制删除)
  • 分页全程使用快照保证数据一致性
  • 相比 Scroll API 更节约内存资源
  • 最多支持 100 个并行 PIT 会话

Kafka学习笔记

Kafka

基本架构

Kafka

副本机制

在 Kafka 中,追随者副本是不对外提供服务的。这就是说,任何一个追随者副本都不能响应消费者和生产者的读写请求。所有的请求都必须由领导者副本来处理,或者说,所有的读写请求都必须发往领导者副本所在的 Broker,由该 Broker 负责处理。追随者副本不处理客户端请求,它唯一的任务就是从领导者副本异步拉取消息,并写入到自己的提交日志中,从而实现与领导者副本的同步。

kafka 为什么这么快

  1. 顺序磁盘 IO
  • kafka 采用追加(append-only)的日志结构,数据总是顺序写入磁盘,顺序读写速度可与内存随机访问相当。
  1. 零拷贝技术
  • 使用 sendfile 系统调用,直接将数据从 page cache 传输到网卡,避免内核态与用户态的数据拷贝
  1. 批量处理
  • Producer:批量发送消息
  • Broker:批量持久化/压缩
  • Consumer:批量拉取数据
  1. 页面缓存优化
  • 使用 OS 的 page cache 缓存数据,减少真实磁盘访问次数
  1. 分区设计:
  • 数据分配存储,支持水平扩展
  • 不同分区可以并行处理
  1. 高效的消息格式
  • 消息集批量存储
  • 二进制协议带来紧凑的数据结构

参数配置

  • auto.create.topics.enable:是否允许自动创建 Topic。建议关闭,否则会出现莫名其妙的主题。

  • unclean.leader.election.enable:是否允许 Unclean Leader 选举。

  • auto.leader.rebalance.enable:是否允许定期进行 Leader 选举。建议设为 false,否则即使工作正常的 leader 也可能别切换。

  • log.retention.{hours|minutes|ms}: 设置一条消息数据保存多长时间

  • log.retention.bytes:这是指定 Broker 为消息保存的总磁盘容量大小。默认为-1,即 kafka 默认不限制

  • message.max.bytes:控制 Broker 能够接收的最大消息大小。

  • partitioner.class 参数:设置分区策略

  • 设置 session.timeout.ms = 6s。即如果 Coordinator 在 6 秒之内没有收到 Group 下某 Consumer 实例的心跳,它就会认为这个 Consumer 实例已经挂了

  • 设置 heartbeat.interval.ms = 2s。发送心跳请求频率的参数

  • max.poll.interval.ms: 限定了 Consumer 端应用程序两次调用 poll 方法的最大时间间隔

  • 你要为你的业务处理逻辑留下充足的时间。这样,Consumer 就不会因为处理这些消息的时间太长而引发 Rebalance 了。

  • max.poll.records: 设置每次 poll 方法返回的消息数。

分区策略

  1. RangeAssignor (范围分区策略)
    • 首先,它对每个主题的分区按照序号进行排序,然后对消费者按照字母顺序进行排序。
    • 然后,它尝试将每个主题的分区均匀地分配给消费者。
  2. RoundRobinAssignor (轮询分区策略)
    • 它将所有主题的所有分区和所有消费者都按照字母顺序排序。
    • 然后,它按照轮询的方式将分区分配给消费者。
  3. StickyAssignor (粘性分区策略)
    • 它在进行分区分配时,会尽量保持之前的分配结果不变。
    • 只有在必要时(如消费者增加或减少)才会进行重新分配。
  4. CooperativeStickyAssignor (协作粘性分区策略)
    • 与 StickyAssignor 类似,它也会尽量保持之前的分配结果不变。
    • 但是,在进行重新分配时,它会采用协作的方式,逐步迁移分区,而不是一次性全部迁移

消息可靠性保障

  • 最多一次(as most once): 消息可能会丢失,但不会重复发送
  • 至少一次(at least once): 消息不会丢失,但有可能被重复发送
  • 精确一次(exactly once): 消息不回丢失,也不会被重复发送

精确一次同通过事务和幂等性来实现

  • 设置 enable.idempotence=true 时,producer 会升级成幂等性
  • 设置 producer 端参数 transactional.id
  • 谁知 isolation_level=read_committed,表明 consumer 只会读取事务型 producer 成功提交事务写入的消息

消费者重平衡

什么情况下会触发重平衡
1: 消费者组中增加或减少消费者
2: 分区增加或减少
3: 主题增加或减少
4: 消费者心跳超时

重平衡期间 kafka 会停止消费消息,直到重平衡完成

如何保证消息不丢失

  1. 生产者端使用带回调的 send 方法
  2. 设置 acks=1 或 all,保证至少写入一个副本
  3. 设置 retries,如设置 retries=3,开启重试机制,避免因网络问题而造成消息丢失。
  4. 设置 replication.factor >=3, broker 提供多个冗余副本
  5. 设置 min.insync.replicas>1, 消息至少写入一个 broker 副本

高水位和 epoch

  • Epoch 确保了 Leader 副本的唯一性和版本正确性。
  • 高水位确保了只有被所有副本确认的消息才能被消费者消费。

简单来说:

  • Epoch 像是对 leader 版本的标识。
  • 高水位像是对数据有效性的标识。

高水位:

定义:

  • 高水位是一个偏移量 (offset),它表示 Kafka 分区中已经被所有副本(包括 Leader 副本和 Follower 副本)成功复制的消息的位置。
  • 换句话说,高水位之前的消息,都被认为已经提交,可以被消费者安全消费。

作用:

  • 消费者只能消费高水位之前的消息,高水位之后的消息对消费者不可见。
  • 保证副本数据一致性: 高水位确保了只有被所有副本确认的消息才能被消费者消费,从而保证了副本之间的数据一致性。

高水位的更新:

  • Leader 副本负责维护高水位。
  • 当 Follower 副本成功复制 Leader 副本的消息时,会向 Leader 副本发送确认信息。
  • Leader 副本根据收到的确认信息,更新高水位。

Epoch:

  • Epoch 是一个单调递增的版本号,用于标识 Leader 副本的变更。
  • 每当发生 Leader 选举时,新的 Leader 副本会生成一个新的 Epoch 值。
  • 每个副本都会维护自己的 Epoch 值。
  • 当 Leader 副本发生变更时,新的 Leader 副本会广播新的 Epoch 值。

作用:

  • 防止数据不一致: 在 Leader 切换时,Epoch 可以防止旧的 Leader 副本上的“陈旧”数据被错误地当作“最新”数据。
  • 确保数据安全性: 当一个分区发生 Leader 选举时,新的 Leader 会生成一个新的 Epoch 值,并将自己的 Offset 作为新的高水位。 这样,即使旧的 Leader 重新加入集群并尝试成为新的 Leader,由于其 Epoch 值较低,将无法通过高水位的校验,从而避免了数据不一致的问 1 题。

备忘

  1. 禁用自动提交位移,改为手动提交。只有当成功消费了消息才提交位移,防止消息丢失。

参考

https://towardsdev.com/kafka-101-a-beginners-guide-to-understanding-kafka-2cd797864614

https://burningmyself.cn/micro/kafka/

为什么 Kafka 这么快?

MySQL学习笔记

MySQL

基本架构

连接器->分析器->优化器->执行器->存储引擎

更新语句的执行流程

image

为什么需要两段提交

由于 redo log 和 binlog 是两个独立的逻辑,如果不用两阶段提交,要么就是先写完 redo log 再写 binlog,或者采用反过来的顺序。我们看看这两种方式会有什么问题。

仍然用前面的 update 语句来做例子。假设当前 ID=2 的行,字段 c 的值是 0,再假设执行 update 语句过程中在写完第一个日志后,第二个日志还没有写完期间发生了 crash,会出现什么情况呢?

  1. 先写 redo log 后写 binlog。假设在 redo log 写完,binlog 还没有写完的时候,MySQL 进程异常重启。由于我们前面说过的,redo log 写完之后,系统即使崩溃,仍然能够把数据恢复回来,所以恢复后这一行 c 的值是 1。
    但是由于 binlog 没写完就 crash 了,这时候 binlog 里面就没有记录这个语句。因此,之后备份日志的时候,存起来的 binlog 里面就没有这条语句。
    然后你会发现,如果需要用这个 binlog 来恢复临时库的话,由于这个语句的 binlog 丢失,这个临时库就会少了这一次更新,恢复出来的这一行 c 的值就是 0,与原库的值不同。
  2. 先写 binlog 后写 redo log。如果在 binlog 写完之后 crash,由于 redo log 还没写,崩溃恢复以后这个事务无效,所以这一行 c 的值是 0。但是 binlog 里面已经记录了“把 c 从 0 改成 1”这个日志。所以,在之后用 binlog 来恢复的时候就多了一个事务出来,恢复出来的这一行 c 的值就是 1,与原库的值不同。

事务的隔离级别

隔离级别 描述
读未提交 事务可以读取未提交的数据
读已提交 事务只能读取已提交的数据
可重复读 事务在开始时看到的数据一致
串行化 事务完全隔离,避免并发冲突

参数

用途 命令
查看事务隔离级别 show variables like 'transaction_isolation';

为什么 MySQL 索引使用 B+树?

  1. 减少 I/O 次数: B+树的非叶子节点只存储索引,不存储数据,可以容纳更多的索引,减少树的高度,从而减少 I/O 次数。
  2. 范围查询效率高: B+树的叶子节点通过指针连接成有序链表,可以方便地进行范围查询。
  3. 磁盘友好: B+树的节点大小通常设置为磁盘页大小,可以一次性加载一个节点到内存,减少磁盘 I/O 次数。

索引

  1. 主键索引: Mysql 会为建立一个主键的索引树,这棵索引树包含了整行的数据
  2. 普通索引: 普通索引存储的是主键 ID
  3. 覆盖索引: 查询的字段已经在索引里包含了,无需回表
  4. 索引下推: 索引里包含的字段值可以在查询的时候直接过滤,减少了回表次数。

为什么应该选择普通索引,而不是唯一索引。

这两者在查询的时候没有区别,但在插入的时候,由于唯一索引需要检查这个值是否存在,就需要从表里把数据加载进来,
这就涉及到磁盘 IO,磁盘 IO 是很慢的,所以唯一索引在插入的时候就明显慢于普通索引。

结合以上两点,应该优先选择普通索引,可考虑在程序中保证唯一性。

索引失效的情况

  1. 在索引列使用了函数计算
  2. 隐式类型转换,也就是索引字段类型和查询值类型不匹配
  3. 不符合最左前缀原则
  4. 使用 like 且通配符在前面
  5. 两个表的连接列需要索引

死锁检测

1
2
3
4
[mysqld]
innodb_print_all_deadlocks = 1
innodb_lock_wait_timeout = 30
innodb_status_output = ON

将全部死锁信息记录到日志中

死锁优化:

  • 确保事务尽可能小而快,减少锁持有时间
  • 保证所有事务以相同的顺序访问表
  • 为大事务考虑拆分多个小事务
  • 优化查询索引,确保事务使用合适的索引

InnoDB 刷新脏页的策略

在 MySQL 的 InnoDB 存储引擎中,为了提高读写效率,数据和索引首先被加载到 Buffer Pool 中。Buffer Pool 是内存中的一块区域,用于缓存数据和索引。当修改数据时,InnoDB 会先修改 Buffer Pool 中的数据页,这些被修改的页称为“脏页”。

什么是脏页?
脏页是指在 Buffer Pool 中被修改过,但尚未同步到磁盘的数据页。当需要读取数据时,InnoDB 会优先从 Buffer Pool 中查找,如果找到所需的页,则直接返回;否则,从磁盘加载数据页。

  1. LRU(Least Recently Used)算法: InnoDB 使用 LRU 算法管理 Buffer Pool 中的数据页。当需要淘汰数据页时,优先淘汰最近最少使用的页。如果最近最少使用的页是脏页,则需要先将其刷新到磁盘。

  2. 后台线程定期刷新: InnoDB 后台有一个专门的线程负责定期将脏页刷新到磁盘。

  3. Redo log 写满: 当 Redo log 写满时,InnoDB 会强制将一部分脏页刷新到磁盘,以腾出空间写入新的 Redo log。

  4. 内存不足: 当系统内存不足时,操作系统会通知 InnoDB,InnoDB 会将一部分脏页刷新到磁盘,以释放内存。

  5. 正常关闭数据库: 在正常关闭数据库时,InnoDB 会将所有脏页刷新到磁盘,以保证数据的一致性。

一旦一个查询请求需要在执行过程中先 flush 掉一个脏页时,这个查询就可能要比平时慢.

为什么删除表数据,表文件大小不变

如果我们用 delete 命令把整个表的数据删除,所有的数据页都会被标记为可复用。但是磁盘上,文件不会变小。

经过大量增删改的表,都是可能是存在空洞的.

可以使用 alter table A engine=InnoDB 命令来重建表,从而达到回收表空间的目的

间隙锁

当查询条件没有使用索引,或者使用了索引但是返回多条记录的情况下,mysql 会使用间隙锁来锁住记录的间隙,来避免之间插入新的数据。

范围查询会使用间隙锁

其他

  1. 可使用show processlist查看连接状态
  2. alter table T engine=InnoDB可重建表索引,提交空间利用率
  3. SELECT * FROM information_schema.INNODB_TRX; 查询当前执行的事务
  4. SHOW ENGINE INNODB STATUS; 查询 INNODB 引擎的状态信息
  5. 主动死锁检测,设置innodb_deadlock_detect=on
  6. SELECT * FROM information_schema.INNODB_LOCKS; 实时锁状态查询
  7. analyze table t 命令,可以用来重新统计索引信息. 解决索引统计信息不准确导致的问题
  8. innodb_io_capacity设置成磁盘的 IOPS,来提高 InnoDB 刷新脏页的速度
  9. innodb_log_file_size: 设置 redo log 文件的大小。
  10. innodb_log_files_in_group: 设置 redo log 文件的数量。
  11. innodb_log_buffer_size: 设置 redo log buffer 的大小
  12. select * from sys.schema_table_lock_waits 或者 innodb_lock_waits 可找出阻塞的 process_id

备忘

如果是可重复读隔离级别,事务 T 启动的时候会创建一个视图 read-view,之后事务 T 执行期间,即使有其他事务修改了数据,事务 T 看到的仍然跟在启动时看到的一样。也就是说,一个在可重复读隔离级别下执行的事务,好像与世无争,不受外界影响。

InnoDB 的数据是按数据页为单位来读写的。也就是说,当需要读一条记录的时候,并不是将这个记录本身从磁盘读出来,而是以页为单位,将其整体读入内存。在 InnoDB 中,每个数据页的大小默认是 16KB。

通常我们说 MySQL 的“双 1”配置,指的就是 sync_binlog 和 innodb_flush_log_at_trx_commit 都设置成 1。也就是说,一个事务完整提交前,需要等待两次刷盘,一次是 redo log(prepare 阶段),一次是 binlog。

sync_binlog=1 的时候,表示每次提交事务都会执行 fsync;
innodb_flush_log_at_trx_commit 设置为 1 的时候,表示每次事务提交时都将 redo log 直接持久化到磁盘;

binlog_format 应该设置成 row,而不是 statement,这样避免主从数据不一致问题,还有方便误删的时候恢复数据。

要避免大事务,会导致主从延迟。删除数据的时候应该批量小部分删除

InnoDB Buffer Pool 的大小是由参数 innodb_buffer_pool_size 确定的,一般建议设置成可用物理内存的 60%~80%。

参考

MySQL 实战 45 讲

Reids学习笔记

Redis 为什么快

  1. 纯内存操作,避免磁盘 IO
  2. 使用了 IO 多路复用机制,无须等待一个慢连接的操作,可以高效的处理多个网络连接
  3. 单线程模型,避免了频繁的上下文切换

Redis 持久性

  1. AOF
    AOF 同步策略:
    1. always: 每执行一条命令就同步到磁盘,数据安全性最高,但性能较差。
    2. everysec: 每秒同步一次,在性能和数据安全性之间取得平衡。
    3. no: 由操作系统决定何时写入磁盘.
  2. RDB
    Redis 快照
    特点:
    1. 文件小,RDB 文件是压缩的二进制文件
    2. 恢复速度快,恢复数据时只需要加载一个文件

影响 Redis 性能的点

  1. 集合的全量查询和聚合操作(可考虑使用 scan 分批读取数据,在客户端操作)
  2. bigkey 删除,大量空间的释放会在内存重新分配时耗时
  3. 清空数据库,flushdb flushall 等,可考虑使用(flushdb async)来异步清空
  4. AOF 日志同步写
  5. 从库加载 RDB 文件
  6. 大量数据的写入会导致 hash 冲突,redis 使用的链式 hash 会导致操作变慢,为避免这个问题,使用了渐进式 hash

针对 big 可以问题的解决方案

如何避免

  1. 拆分 bigkey,将一个大的 key 拆分成多个小的 key
  2. 选择合理的数据结构
  3. 水平分片,将数据分配到多个 redis 实例

删除

  1. 使用 unlink 异步删除
  2. 渐进式删除,每次分批删除一部分,可以使用 scan,hscan,zscan 等

redis6.0 为什么引入多线程,不是已经有了 IO 多路复用吗

Redis 6.0 引入多线程的原因主要在于优化网络 IO 性能,而不是替代多路 IO 复用机制。简要来说:

1️. 多路 IO 复用(如 epoll)仍然承担着管理客户端连接和事件分发的核心工作,确保单线程模型带来的简单性和原子性;

2️. 多线程被引入后,用于并行处理网络数据的读写(如请求数据的读取与响应数据的发送),从而减少了数据在单线程处理时的瓶颈,尤其在处理大 value 或高并发大量数据传输时提升了吞吐量;

3️. 总体上:命令解析和执行依旧保持单线程来维护数据一致性,而多线程仅用于网络数据 IO 的辅助处理,二者协同工作,实现了性能和稳定性的平衡。

网络 I/O 并行化:

多个 I/O 线程: Redis 6.0 将网络请求的处理分配给多个 I/O 线程,每个线程负责处理一部分连接。
并行处理: 这样,多个客户端的请求可以并行处理,提高了网络 I/O 的吞吐量。
减少阻塞: 一个慢速的请求不会阻塞其他请求,提高了系统的整体响应速度。

Redis 循序渐进

为了防止单个实例宕机,引入主从。为了防止主库宕机无法提供服务,引入哨兵机制来实现主从切换。
为了防止哨兵宕机和误判,引入了哨兵集群。

为了在数据量过大时单机实例无法容纳或者性能下降的问题,引入切片集群,将数据分散到多个实例上。

Redis 变慢问题排查

  1. 是否阻塞性操作,key *命令,或同步删除 bigkey
  2. 是否开启了 AOF 日志并同步写
  3. 查看是否发生内存 swap,是否数据量太大导致了 swap,可以考虑增加物理内存

https://freegeektime.com/100056701/287819/

缓存淘汰策略

Redis7.0 的默认缓存淘汰策略是noeviction, 这意味着当 Redis 的内存使用量达到设置的最大值时,新的写入操作将会失败,而不是去主动淘汰旧的数据。

可通过命令设置其他的淘汰策略,如 LRU

config set maxmemory-policy volatile-lru

缓存更新策略

  1. Cache-Aside: 先更新数据库再删除缓存
  2. Read Through: 应用先查询缓存,如果缓存中没有数据,就查询数据库并写入缓存。如果缓存有数据就直接返回。
  3. Write Through: 应用先更新数据库,再更新缓存。缺点:缓存更新失败会导致数据一致性问题。可加入重试机制。
  4. Write Behind: 异步写回。应用先更新缓存,异步将更新操作写入消息队列,消费者从消息队列取更新操作写入数据库
  5. Write Around: 写绕过。写操作直接更新数据库。读操作先从缓存取,如果没有查询数据库并更新缓存。缺点:缓存更新滞后,导致数据一致性问题。

参考

Redis 核心技术与实战

Redis 多线程网络模型全面揭秘

go压力测试工具

hey 是一个用 Go 编写的 HTTP 压力测试工具,可以模拟大量并发请求。

https://github.com/rakyll/hey

1
2
go install github.com/rakyll/hey@latest
hey -n 10000 -c 1000 http://localhost:8080/books // 10000 个请求,1000 个并发

/images/go-hey-benchmark.png

go防火墙弹窗

在本地运行 go 应用程序的时候,默认情况下会导致系统防火墙弹窗

/images/go-gin-port-firewall-window.png

需要检查你的 Go 应用程序是否正在监听所有网络接口(例如使用 0.0.0.0 或::)。这可能会触发防火墙弹窗。建议改为监听本地回环地址 127.0.0.1,除非你的应用程序需要接受来自外部的连接。

我们的应用程序不需要接受来自外部的连接,所以只需要监听 127.0.0.1 就可以了

/images/go-gin-without-alter.png