缓存池 bytebufferpool 库实现原理

上一节《Runtime: Golang 之 sync.Pool 源码分析》我们介绍了sync.Pool 的源码分析,本节介绍一个 fasthttp 中引用的一缓存池库 bytebufferpool,这两个库是同一个开发者。对于这个缓存池库与同类型的几个库的对比,可以参考 https://omgnull.github.io/go-benchmark/buffer/

建议大家了解一下fasthttp 这个库,性能要比直接使用内置的 net/http 高出很多,其主要原因是大量的用到了缓存池 sync.Pool 进行性能提升。

用法

// https://github.com/valyala/bytebufferpool/blob/18533face0/bytebuffer_example_test.go
package bytebufferpool_test

import (
	"fmt"

	"github.com/valyala/bytebufferpool"
)

func ExampleByteBuffer() {
	// 从缓存池取 Get()
	bb := bytebufferpool.Get()

	// 用法
	bb.WriteString("first linen")
	bb.Write([]byte("second linen"))
	bb.B = append(bb.B, "third linen"...)

	fmt.Printf("bytebuffer contents=%q", bb.B)

	// 使用完毕,放回缓存池 Put()
	// It is safe to release byte buffer now, since it is no longer used.
	bytebufferpool.Put(bb)
}

全局变量

我们先看一下与其相关的一些常量

const (
	// 定位数据索引位置,使用位操作性能比较高效
	minBitSize = 6 // 2**6=64 is a CPU cache line size
	// 数组索引个数 0~19
	steps      = 20

	// 最小缓存对象 和 最大缓存对象大小
	minSize = 1 << minBitSize
	maxSize = 1 << (minBitSize + steps - 1)

	// 校准阈值, 这里指的调用次数
	calibrateCallsThreshold = 42000
	// 百分比,校准数据基数
	maxPercentile           = 0.95
)

对于常量上面已做了注释,如果现在不明白的话没有关系,看完下面就知道它们的作用了。

数据类型

主要有两个相关的数据结构,分别为 PoolByteBuffer,其实现也比较的简单。

数据结构

// ByteBuffer provides byte buffer, which can be used for minimizing
// memory allocations.
//
// ByteBuffer may be used with functions appending data to the given []byte
// slice. See example code for details.
//
// Use Get for obtaining an empty byte buffer.
type ByteBuffer struct {

	// B is a byte buffer to use in append-like workloads.
	// See example code for details.
	B []byte
}

// Pool represents byte buffer pool.
//
// Distinct pools may be used for distinct types of byte buffers.
// Properly determined byte buffer types with their own pools may help reducing
// memory waste.
type Pool struct {
	calls       [steps]uint64
	calibrating uint64

	defaultSize uint64
	maxSize     uint64

	pool sync.Pool
}

var defaultPool Pool

字段解释

  • calls 缓存对象大小调用次数统计,steps 就是我们上面定义的常量。主要用来统计每类缓存大小的调用次数。steps 具体的值会使用一个 index() 函数通过位操作的方式计算出来它在这个数组的索引位置;
  • calibrating 校标标记。0 表示未校准,1表示正在校准。校准需要一个过程,校准完成后需要从1恢复为 0;
  • defaultSize 缓存对象默认大小。我们知道当从 pool 中获取缓存对象时,如果池中没有对象可取,会通过调用 一个 New() 函数创建一个新对象返回,这时新创建的对象大小为 defaultSize。当然这里没有使用 New() 函数,而是直接创建了一个 指定默认大小的 ByteBuffer
  • maxSize 允许放入pool池中的最大对象大小,只有<maxSize 的对象才允许放放池中

这里的变量 defaultPool 是一个全局的 Pool 对象。

Continue reading

初识kubernetes 组件

对于一个刚刚接触kubernetes(k8s)的新手来说,想好更好的学习它,首先就要对它有一个大概的认知,所以本文我们先以全局观来介绍一个kubernetes。

kubernetes架构

8ee9f2fa987eccb490cfaa91c6484f67
kubernetes 架构图

kubernets整体可以分为两大部分,分别为 MasterNode ,我们一般称其为节点,这两种角色分别对应着控制节点和计算节点,根据我们的经验可以清楚的知道 Master 是控制节点。

Master 节点

控制节点 Master 节点由三部分组成,分别为 Controller ManagerAPI ServerScheduler ,它们相互紧密协作,每个部分负责不同的工作职责。

  • controller-manager 全称为 kube-controler-manager 组件,主要用来负责容器编排。如一个容器(实际上是pod,pod是最基本的调度单元。一般一个pod里会部署一个容器服务)服务可以指定副本数量,如果实际运行的副本数据与期望的不一致,则会自动再启动几个容器副本,最终实现期望的数量。这个组件,就是一系列控制器的集合。我们可以查看一下 Kubernetes 项目的 pkg/controller 目录, 伪代码如下:
for {
  实际状态 := 获取集群中对象X的实际状态(Actual State)
  期望状态 := 获取集群中对象X的期望状态(Desired State)
  if 实际状态 == 期望状态{
    什么都不做
  } else {
    执行编排动作,将实际状态调整为期望状态
  }
}
  • api server 对外提供api服务,用来接收命令进行集群管理。对内负责与etcd注册中心进行通讯,进行一些配置信息的存储与读取
  • scheduler 负责调度。如一个容器存放到k8s集群中的哪个node节点最为合适

实际上这三个节点的功能远远多于我们描述的。

Continue reading

docker如何利用cgroup对容器资源进行限制

在容器里有两个非常重要的概念,一个是 namespace 用来实现对容器里所有进程进行隔离;另一个就是 cgroup,用来对容器进程内使用资源进行限制。那 cgroup 又是如何实现对资源进行限制的呢,今天我们来了解一下它的实现原理。

什么是cgroup

cgroupControl Groups 的缩写,是 Linux 内核提供的一种可以限制、记录、隔离 进程组 所使用的物理资源(如 cpu、memory、磁盘IO等等) 的机制,被 LXCdocker 等很多项目用于实现进程资源控制。cgroup 是将任意进程进行分组化管理的 Linux 内核功能。
cgroup 本身是提供将进程进行分组化管理的功能和接口的基础结构,I/O 或内存的分配控制等具体的资源管理功能是通过这个功能来实现的。 一定要切记,这里的限制单元为 进程组,而不是进程。

Continue reading

Golang 内存组件之mspan、mcache、mcentral 和 mheap 数据结构

Golang中的内存组件关系如下图所示

components of memory allocation
golang 内存分配组件

在学习golang 内存时,经常会涉及几个重要的数据结构,如果不熟悉它们的情况下,理解起来就显得格外的吃力,所以本篇主要对相关的几个内存组件做下数据结构的介绍。

在 Golang 中,mcachemspanmcentralmheap 是内存管理的四大组件,mcache 管理线程在本地缓存的 mspan,而 mcentral 管理着全局的 mspan 为所有 mcache 提供所有线程。

根据分配对象的大小,内部会使用不同的内存分配机制,详细参考函数 mallocgo()

  • <16KB 会使用微小对象内存分配器从 P 中的 mcache 分配,主要使用 mcache.tinyXXX 这类的字段
  • 16-32KBP 中的 mcache 中分配
  • >32KB 直接从 mheap 中分配

对于golang中的内存申请流程,大家应该都非常熟悉了,这里不再进行详细描述。

Golang 内存组件关系

mcache

在GPM关系中,会在每个 P 下都有一个 mcache 字段,用来表示内存信息。

在 Go 1.2 版本以前调度器使用的是 GM 模型,将 mcache 放在了 M 里,但发现存在诸多问题,其中对于内存这一块存在着巨大的浪费。每个 M 都持有 mcachestack alloc,但只有在 M 运行 Go 代码时才需要使用内存(每个 mcache 可以高达2mb),当 M 在处于 syscall网络请求 的时候是不需要内存的,再加上 M 又是允许创建多个的,这就造成了内存的很大浪费。所以从go 1.3版本开始使用了GPM模型,这样在高并发状态下,每个G只有在运行的时候才会使用到内存,而每个 G 会绑定一个P,所以它们在运行只占用一份 mcache,对于 mcache 的数量就是P 的数量,同时并发访问时也不会产生锁。

对于 GM 模型除了上面提供到内存浪费的问题,还有其它问题,如单一全局锁 sched.Lock、goroutine 传递问题和内存局部性等。

P 中,一个 mcache 除了可以用来缓存小对象外,还包含一些本地分配统计信息。由于在每个P下面都存在一个mcache ,所以多个 goroutine (每个P运行一个g)并发请求内存时是无锁的。

Continue reading

GC 对根对象扫描实现的源码分析

工作池gcWork

工作缓存池(work pool)实现了生产者和消费者模型,用于指向灰色对象。一个灰色对象在工作队列中被扫描标记,一个黑色对象表示已被标记不在队列中。

写屏障、根发现、栈扫描和对象扫描都会生成一个指向灰色对象的指针。扫描消费时会指向这个灰色对象,从而将先其变为黑色,再扫描它们,此时可能会产生一个新的指针指向灰色对象。这个就是三色标记法的基本知识点,应该很好理解。

gcWork 是为垃圾回收器提供的一个生产和消费工作接口。

它可以用在stack上,如

(preemption must be disabled)
gcw := &getg().m.p.ptr().gcw
.. call gcw.put() to produce and gcw.tryGet() to consume ..

在标记阶段使用gcWork可以防止垃圾收集器转换到标记终止,这一点很重要,因为gcWork可能在本地持有GC工作缓冲区。可以通过禁用抢占(systemstackacquirem)来实现。

Continue reading

Runtime: Golang GC源码分析

在阅读此文前,需要先了解一下三色标记法以及混合写屏障这些概念。

源文件 src/runtime/mgc.go 版本 1.16.2。

基本知识

在介绍GC之前,我们需要认识有些与GC相关的基本信息,如GC的状态、模式、统计信息等。

三种状态

共有三种状态

const (
	_GCoff             = iota // GC not running; sweeping in background, write barrier disabled
	_GCmark                   // GC marking roots and workbufs: allocate black, write barrier ENABLED
	_GCmarktermination        // GC mark termination: allocate black, P's help GC, write barrier ENABLED
)
  • _GCoff GC未运行
  • _GCmark 标记中,启用写屏障
  • _GCmarktermination 标记终止,启用写屏障

三种模式

支持三种模式:

const (
    gcBackgroundMode gcMode = iota // concurrent GC and sweep
    gcForceMode                    // stop-the-world GC now, concurrent sweep
    gcForceBlockMode               // stop-the-world GC now and STW sweep (forced by user)
)
  • gcBackgroundMode 默认模式,标记与清扫过程都是并发执行的
  • gcForceMode 只在清扫阶段支持并发;
  • gcForceBlockMode GC全程需要STW。

针对每种模式,在标记阶段会采用不同的标记策略,详细见  gcBgMarkWorker() 

Continue reading

Golang中的切片与GC

今天再看 timer 源码的时候,在函数 clearDeletedTimers() 里看到一段对切片的处理代码,实现目的就是对一个切片内容进行缩容。

// src/runtime/time.go

// The caller must have locked the timers for pp.
func clearDeletedTimers(pp *p) {
	timers := pp.timers
	......
	// 对无用的切片元素赋值 nil
	for i := to; i < len(timers); i++ {
		timers[i] = nil
	}

	atomic.Xadd(&pp.deletedTimers, -cdel)
	atomic.Xadd(&pp.numTimers, -cdel)
	atomic.Xadd(&pp.adjustTimers, -cearlier)

	timers = timers[:to]
	pp.timers = timers
	updateTimer0When(pp)

	......
}

变量 to 指新切片的长度, len(timers)指原来切片的长度。

这里在其进行 timers = timers[:to] 操作前,先是将 to 数组索引后的值进行了赋值 nil。按照我们平常的用法,赋 nil 值是没有必要执行这一步的,那为什么这里要加这一步呢?主要还是与GC 有关。

在日常开发中很少注意到这个细节,虽然最终实现的结果是一样的,但如果考虑GC的话,差别可就很大了。

Continue reading