补充
---------------20230424补充--------------------- GIN
什么是池化
提前准备一些资源,在需要时可以重复使用这些预先准备的资源。
通俗的讲,池化技术就是:把一些资源预先分配好,组织到资源池中,之后的业务可以直接从资源池中获取,使用完后放回到资源池中
好处
- 减少资源重复使用, 减少了资源分配和释放过程中的系统消耗。
- 限制资源的使用量 。比如线程池中的Core和Max
- 避免内存碎片化。 由于资源通常是集中分配的,避免了内存被碎片化占用
常见的池
协程池
连接池(Redis MySQL)
连接池包含数据库连接池、Redis连接池、Http连接池等
资源池(Locker WaitGroup ByteBuffer)
使用&不使用 对比
bytesBuffer池
package benchmark_newStruct
import (
"bytes"
"io/ioutil"
"sync"
"testing"
)
var bytesBufferPool = sync.Pool{
New: func() interface{} {
return &bytes.Buffer{}
},
}
var (
fileName = "$(pwd)/benchmark/benchmark_pool/test.txt"
data = make([]byte, 10000) //字节长度
)
func newBytesBuffer() *bytes.Buffer {
return new(bytes.Buffer)
}
func BenchmarkWriteFile(b *testing.B) {
for n := 0; n < b.N; n++ {
buf := newBytesBuffer()
buf.Reset() // Reset 缓存区,不然会连接上次调用时保存在缓存区里的内容
buf.Write(data)
_ = ioutil.WriteFile(fileName, buf.Bytes(), 0644)
}
}
func BenchmarkWriteFileWithPool(b *testing.B) {
for n := 0; n < b.N; n++ {
buf := bytesBufferPool.Get().(*bytes.Buffer) // 如果是第一个调用,则创建一个缓冲区
buf.Reset() // Reset 缓存区,不然会连接上次调用时保存在缓存区里的内容
buf.Write(data)
_ = ioutil.WriteFile(fileName, buf.Bytes(), 0644)
bytesBufferPool.Put(buf) // 将缓冲区放回 sync.Pool中
}
}
- 10^3字节
- 10^4字节
- 10^5字节
- 10^7
可以看到,单次op的耗时降低从1/3 -> 1/10 , 单次op对内存消耗降低99%
Kafka
发送消息
我们先看一下客户端每条消息的元数据构成 ProducerRecode
其中灰色部分可能为空
发送消息的过程
Kafka为了提高吞吐量,Producer采用批量发送数据的模式 ,其Producer 和Server的交互过程中使用了消息累加器和客户端内存池来提高吞吐量
Kafka参数
buffer.memory= //内存池最大占用内存大小 默认32MB
batch.size= //每个Batch的大小 默认16KB
消息累加器
Kafka生产端将消息聚合成一个Batch发送
常见的Batch聚合
但是Kafka有一个Batch队列的概念,每个Batch都添加到一个Deque(双端队列)中
新消息会添加到deque中的最后一个Batch中,当Batch达到配置的Batch.size或者linger.ms上限后,该Batch内的消息发送至Sender线程
同时,Deque在内存中常驻,因此当Batch中的消息被发送到Sender线程后,Batch不会直接被JVM回收,而是会等待一定时间(默认1分钟) 未被使用后放入内存池中
内存池设计
kafka客户端内存池不是JVM维护的,而是由Kafka手动维护的
- 对象设计
public class BufferPool {
//总共的内存大小
private final long totalMemory;
//每一个producerbatch的默认大小
private final int poolableSize;
//锁对象,用于保证多生产者获取内存的线程安全
private final ReentrantLock lock;
//已经从jvm中申请的内存,但是目前没有使用
private final Deque<ByteBuffer> free;
//等待申请的内存的生产者的condition队列
private final Deque<Condition> waiters;
//还剩余可用的内存空间大小,totalMemory = free.size() * poolableSize + nonPooledAvailableMemory
private long nonPooledAvailableMemory;
//记录器
private final Metrics metrics;
//kafka自带的时间类
private final Time time;
//等待时间的类,和Metrics一起使用记录等待时长
private final Sensor waitTime;
//是否关闭标志位
private boolean closed;
}
nonPooledAvailableMemory指的就是非缓冲池的可用内存大小。
非缓冲池分配内存,其实就是调用ByteBuffer.allocat分配真实的JVM内存
消息发送流程
引入内存池之后 消息发送的大概流程
流程图
总结
- Kafka以Batch为最小单位发送消息
- Batch实际是一段定长的byte数组,对该数组的所有操作仅支持追加且每次追加都会加锁
- Kafka通过内存池的方式对每个Batch进行内存管理,避免频繁进行内存的申请、Batch对象的创建和销毁
思考
Q1: 我们看到,在kafka当中,对于内存申请 无处不在公平,包括deque也一样,公平的双向队列 先进先出,而没有引入优先队列去实现类似权重的功能 Why?
A1: 我理解对于消息发送这个场景,每一条消息都是公平的,不存在哪条消息更高优,如果需要引入权重的概念,那可以做back up ,去单独搭建高优消息的Kafka集群
Broker
在Kafka的架构中,会有很多客户端向Broker端发送请求,Kafka 的 Broker 端有个 SocketServer 组件,用来和客户端建立连接,然后通过Acceptor线程来进行请求的分发,由于Acceptor不涉及具体的逻辑处理,非常得轻量级,因此有很高的吞吐量。
接着Acceptor 线程采用轮询的方式将入站请求公平地发到所有网络线程中
网络线程池
网络线程池默认大小是 3个,表示每台 Broker 启动时会创建 3 个网络线程,专门处理客户端发送的请求,可以通过Broker 端参数 num.network.threads来进行修改。
当网络线程拿到请求后,会将请求放入到一个共享请求队列中。
IO线程池
同时,Broker 端还有个 IO 线程池,负责从该队列中取出请求,执行真正的处理。如果是 Produce 生产请求,则将消息写入到底层的磁盘日志中;如果是 Fetch 请求,则从磁盘或页缓存中读取消息。
IO 线程池处中的线程是执行请求逻辑的线程,默认是8,表示每台 Broker 启动后自动创建 8 个 IO 线程处理请求,可以通过Broker 端参数 num.io.threads调整
GIN
gin的Context在主引擎中是以sync.Pool的方式完成初始化的,也就是说一开始不会有任何Context存在,直到第一个请求过来时才会初始化一个Context,后续的请求有可能会复用之前的Context,这就解释了为什么会有一个 reset() 方法。
func New() *Engine {
engine := &Engine{
...
}
//初始化生成一个Context的方法
engine.pool.New = func() any {
return engine.allocateContext(engine.maxParams)
}
return engine
}
//生成一个gin的Context
func (engine *Engine) allocateContext(maxParams uint16) *Context {
v := make(Params, 0, maxParams) //声明一个Params的切片,并且直接定义好容量
skippedNodes := make([]skippedNode, 0, engine.maxSections)
//返回一个gin的Context,注意此时的Context还不完整,不能直接使用。
return &Context{engine: engine, params: &v, skippedNodes: &skippedNodes}
}
//真正获取Context,并且完成初始化的地方。
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
//从池子里拿到一个Context,此时没人能够保证这个Context是新的还是别人用过的。
c := engine.pool.Get().(*Context)
//将两个最核心的部分塞进Context。
c.writermem.reset(w)
c.Request = req
//重置下Context其他属性,无论他是不是二手的,这里都可以保证它是新的。
c.reset()
//开始处理HTTP请求。
engine.handleHTTPRequest(c)
//处理完成,将当前用过的Context再放回池子里。
engine.pool.Put(c)
}
总结
- 池化思想是服务端性能优化的一个方法,通常能给我们带来较高的性能提升,但是同时为了维护这个池子会带来额外的消耗,通常来说这个消耗都是可以被接受的