golang基础

map和slice的扩容机制

slice扩容

slice扩容主要依赖于growslice函数以下是growslice函数关于扩容部分的细节

  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
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
func growslice(oldPtr unsafe.Pointer, newLen, oldCap, num int, et *_type) slice {
	oldLen := newLen - num
	if newLen < 0 {
		panic(errorString("growslice: len out of range"))
	}

	if et.Size_ == 0 {
		// append should not create a slice with nil pointer but non-zero len.
        // append 不应该创建一个空指针但不为0长度的切片
		// We assume that append doesn't need to preserve oldPtr in this case.
        //我们假设append在这里不需要维护旧的slice指针
		return slice{unsafe.Pointer(&zerobase), newLen, newLen} //返回一个
	}

	newcap := nextslicecap(newLen, oldCap) //nextsliceap负责计算下一个合适切片长度

	var overflow bool
	var lenmem, newlenmem, capmem uintptr
	// Specialize for common values of et.Size.
	// For 1 we don't need any division/multiplication.
	// For goarch.PtrSize, compiler will optimize division/multiplication into a shift by a constant.
	// For powers of 2, use a variable shift.
	noscan := !et.Pointers() //类型为指针?
    
    //容量计算阶段
	switch {
	case et.Size_ == 1:
		lenmem = uintptr(oldLen)
		newlenmem = uintptr(newLen)
		capmem = roundupsize(uintptr(newcap), noscan)
		overflow = uintptr(newcap) > maxAlloc
		newcap = int(capmem)
	case et.Size_ == goarch.PtrSize: //获取当前设备平台的指针长度,并判断与该类型长度是否相同
		lenmem = uintptr(oldLen) * goarch.PtrSize
		newlenmem = uintptr(newLen) * goarch.PtrSize
		capmem = roundupsize(uintptr(newcap)*goarch.PtrSize, noscan)
		overflow = uintptr(newcap) > maxAlloc/goarch.PtrSize
		newcap = int(capmem / goarch.PtrSize)
	case isPowerOfTwo(et.Size_): //isPowerOfTwo 检查输入值是否是2的幂,
		var shift uintptr
		if goarch.PtrSize == 8 {
			// Mask shift for better code generation.
			shift = uintptr(sys.TrailingZeros64(uint64(et.Size_))) & 63
		} else {
			shift = uintptr(sys.TrailingZeros32(uint32(et.Size_))) & 31
		}
		lenmem = uintptr(oldLen) << shift
		newlenmem = uintptr(newLen) << shift
		capmem = roundupsize(uintptr(newcap)<<shift, noscan)
		overflow = uintptr(newcap) > (maxAlloc >> shift)
		newcap = int(capmem >> shift)
		capmem = uintptr(newcap) << shift
	default:	//其他情况
		lenmem = uintptr(oldLen) * et.Size_
		newlenmem = uintptr(newLen) * et.Size_
		capmem, overflow = math.MulUintptr(et.Size_, uintptr(newcap))
		capmem = roundupsize(capmem, noscan)
		newcap = int(capmem / et.Size_)
		capmem = uintptr(newcap) * et.Size_
	}

	// The check of overflow in addition to capmem > maxAlloc is needed
	// to prevent an overflow which can be used to trigger a segfault
	// on 32bit architectures with this example program:
	//
	// type T [1<<27 + 1]int64
	//
	// var d T
	// var s []T
	//
	// func main() {
	//   s = append(s, d, d, d, d)
	//   print(len(s), "\n")
	// }
    //检查溢出标志位或者容量大小是否大于最大可分配值
	if overflow || capmem > maxAlloc {
		panic(errorString("growslice: len out of range"))
	}

	var p unsafe.Pointer //新切片地址
	if !et.Pointers() {
		p = mallocgc(capmem, nil, false) //初始化p地址上的内存
		// The append() that calls growslice is going to overwrite from oldLen to newLen.
		// Only clear the part that will not be overwritten.
		// The reflect_growslice() that calls growslice will manually clear
		// the region not cleared here.
		memclrNoHeapPointers(add(p, newlenmem), capmem-newlenmem)
	} else {
		// Note: can't use rawmem (which avoids zeroing of memory), because then GC can scan uninitialized memory.
		p = mallocgc(capmem, et, true) //初始化p地址上的内存
		if lenmem > 0 && writeBarrier.enabled {
			// Only shade the pointers in oldPtr since we know the destination slice p
			// only contains nil pointers because it has been cleared during alloc.
			//
			// It's safe to pass a type to this function as an optimization because
			// from and to only ever refer to memory representing whole values of
			// type et. See the comment on bulkBarrierPreWrite.
			bulkBarrierPreWriteSrcOnly(uintptr(p), uintptr(oldPtr), lenmem-et.Size_+et.PtrBytes, et)
		}
	}
	memmove(p, oldPtr, lenmem) //拷贝旧地址上切片数据到新地址

	return slice{p, newLen, newcap} //初始化新切片数据返回
}


func nextslicecap(newLen, oldCap int) int {
	newcap := oldCap
	doublecap := newcap + newcap 
	if newLen > doublecap { //尝试翻倍旧容量,双倍旧容量仍然装不下就直接把新长度定为新容量
		return newLen
	}

	const threshold = 256 //配置速率变化阈值
	if oldCap < threshold {
		return doublecap //阈值小于256且新长度小于双倍原容量,直接返回双倍原容量
	}
	for {
		// Transition from growing 2x for small slices
		// to growing 1.25x for large slices. This formula
		// gives a smooth-ish transition between the two.
        //过渡变化增长速率,小切片双倍扩容对于大切片1.25倍扩容
		newcap += (newcap + 3*threshold) >> 2

		// We need to check `newcap >= newLen` and whether `newcap` overflowed.
		// newLen is guaranteed to be larger than zero, hence
		// when newcap overflows then `uint(newcap) > uint(newLen)`.
		// This allows to check for both with the same comparison.
        //我们需要检查新容量始终大于新长度和检查新容量是否溢出。新长度必然大于0,因此当新容量溢出那么新容量大于新长度。这需要允许检查是否等于的情况
		if uint(newcap) >= uint(newLen) {
			break
		}
	}

	// Set newcap to the requested cap when
	// the newcap calculation overflowed.
	if newcap <= 0 {
		return newLen
	}
	return newcap
}

slice扩容过程:触发扩容后,函数会通过计算得到一个扩容后的切片长度,对一段内存进行初始化,然后把原切片的数据直接通过内存拷贝拷到新切片的内存地址中,然后返回一个新切片。

map扩容

golang的map结构是使用链表来解决哈希冲突

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
type hmap struct {
	count     int //map大小
	flags     uint8	//标志位
	B         uint8  // log2的值
	noverflow uint16 // 溢出的bucket熟练
	hash0     uint32 // 哈希种子

	buckets    unsafe.Pointer // 2的B次方个桶的列表,可能为nil或者数量为0
	oldbuckets unsafe.Pointer // 指向旧桶数组的指针,仅在哈希表扩容时非nil。旧桶数组的大小是新桶数组的一半。
	nevacuate  uintptr        // progress counter for evacuation (buckets less than this have been evacuated)

	extra *mapextra // optional fields
}
 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
44
45
46
47
48
49
50
51
52
53
54
func hashGrow(t *maptype, h *hmap) {
	// If we've hit the load factor, get bigger.
	// Otherwise, there are too many overflow buckets,
	// so keep the same number of buckets and "grow" laterally.
	bigger := uint8(1)
	if !overLoadFactor(h.count+1, h.B) { //检查负载因子,当前map负载是否过高
		bigger = 0
		h.flags |= sameSizeGrow
	}
	oldbuckets := h.buckets //拷贝原map的桶
	newbuckets, nextOverflow := makeBucketArray(t, h.B+bigger, nil)

	flags := h.flags &^ (iterator | oldIterator)
	if h.flags&iterator != 0 {
		flags |= oldIterator
	}
	// commit the grow (atomic wrt gc) 原子提交map的各项数据
	h.B += bigger
	h.flags = flags
	h.oldbuckets = oldbuckets
	h.buckets = newbuckets
	h.nevacuate = 0
	h.noverflow = 0

	if h.extra != nil && h.extra.overflow != nil {
		// Promote current overflow buckets to the old generation.
		if h.extra.oldoverflow != nil {
			throw("oldoverflow is not nil")
		}
		h.extra.oldoverflow = h.extra.overflow
		h.extra.overflow = nil
	}
	if nextOverflow != nil {
		if h.extra == nil {
			h.extra = new(mapextra)
		}
		h.extra.nextOverflow = nextOverflow
	}

	// the actual copying of the hash table data is done incrementally
	// by growWork() and evacuate().
}

func growWork(t *maptype, h *hmap, bucket uintptr) {
	// make sure we evacuate the oldbucket corresponding
	// to the bucket we're about to use
    //确保我们迁移了我们将要使用的桶对应的旧桶
	evacuate(t, h, bucket&h.oldbucketmask())

	//如果当前桶正在扩容,再次迁移一次
	if h.growing() {
		evacuate(t, h, h.nevacuate)
	}
}

map的扩容机制主要涉及与两个方面

  1. 负载因子过大,哈希表扩容

    哈希表的扩容有一个优化思路不是直接扩容,不是直接初始化内存块,而是等到需要时分片初始化,而且需要时才会触发迁移,不然直接在大map时直接迁移引起性能问题。

  2. 溢出桶过多,桶扩容 新建桶集,将原桶集标记为旧集,新桶集成为当前桶集。将溢出承受桶链到已经溢出的桶后

MySQL优化

mysql遇到性能瓶颈表现为执行sql慢,我们需要查看慢查询日志来查询究竟是那条sql执行缓慢,慢查询日志打开 mysql常见优化思路:

  1. 优化sql,sql写的不对或可使用跟高级更高性能语法优化

  2. 索引优化,通过索引来优化查询,比如部分sql如多select嵌套使用join优化可以利用到索引,还有order bygroup by

B树和B+树? 更加贴近于磁盘io的树,通过缩短树的层树来尽可能减少磁盘io速度,而b树与b+树区别是b+树内部节点不存储实际结构,数据都在叶子节点上。b类树的目的是通过树结构的分支来减少不必要的查询和尽可能减少io次数,如果采用深树,减少了理论时间复杂度,但频繁进行io操作的性能远低于低层树。

mysql事务

Redis

缓存穿透

穿透即访问透过缓存(缓存内没有)访问到数据库,但是数据库也没有,加大了数据库压力,如果对于异常流量须在网关进行相关限流

缓存雪崩

雪崩,大量缓存过期(即类似雪山崩塌),访问全部进入数据库,常见于热点数据过期,通常使用随机过期来避免

缓存击穿

击穿更为危险,常常针对于热点数据(经常方法的数据)过期,大量请求直击数据库,数据库宕机

Redis 持久化

AOF

类似于MYSQL的binlog,记录了redis的所有操作记录,这个记录随着redis运行时间变长会越来越大,当出现灾难数据丢失时,使用AOF的文件进行恢复,过程就是重复执行一遍在丢失数据前的所有redis操作指令。缺点时恢复数据时执行那些命令需要耗时,比不上RDB备份的直接恢复。

RDB

Redis内容的全备份,备份所有数据的二进制数据,速度块。缺点是仅仅记录了当前内容,可能丢失数据。

Pipeline

Redis提供的一种多命令一次发送并且返回结果集的一种接口,本质上就是把多个请求打包在一个请求里,以此来节省网络开销,尤其是在弱网环境下

CI/CD

jetlinks

Docker

网络桥接

Gin框架源码构成

Gin框架不是一款基于TCP SOCKET封装的完全HTTP服务框架,他是基于net/http标准库二次开发的一款框架,在标准库上扩展了功能,重写了路由树。对于HTTP底层的链接管理,IO读写等等,均由http库提供。

Engine

 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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
type Engine struct {
	RouterGroup

	// RedirectTrailingSlash enables automatic redirection if the current route can't be matched but a
	// handler for the path with (without) the trailing slash exists.
	// For example if /foo/ is requested but a route only exists for /foo, the
	// client is redirected to /foo with http status code 301 for GET requests
	// and 307 for all other request methods.
	// RedirectTrailingSlash 启用了自动重定向,如果当前路由不能匹配但存在带有或不带有尾随斜杠的路由处理程序,则客户端将重定向到 /foo,并且对所有其他请求方法返回307。
	RedirectTrailingSlash bool

	// 首先删除多余的路径元素,例如../或//。
	// 然后检查已清理路径的大小写。
	// 如果找到该路由的处理程序,则路由器将使用状态代码301的GET请求或所有其他请求方法重定向到修正路径。
	// 例如/FOO和/..//Foo可以重定向到/foo。
	// RedirectTrailingSlash和RedirectFixedPath是独立的。
	RedirectFixedPath bool

	
    //HandleMethodNotAllowed参数如果被启用,如果当前的请求无法被路由,路由会检查其他方法是否被允许。
    //如果能找到其他与之匹配的方法路由,则返回405“此方法不被允许”。如果没有其他方法与之匹配,则直接返回404
	HandleMethodNotAllowed bool

	// ForwardedByClientIP if enabled, client IP will be parsed from the request's headers that
	// match those stored at `(*gin.Engine).RemoteIPHeaders`. If no IP was
	// fetched, it falls back to the IP obtained from
	// `(*gin.Context).Request.RemoteAddr`.
    //ForwardedByClientIP启用时,客户端IP将从请求的标头中解析,这些标头与存储在(*gin.Engine).RemoteIPHeaders中的标头匹配。如果没有找到IP,他会继续从(*gin.Context).Request.RemoteAddr中获得IP
	ForwardedByClientIP bool

	// AppEngine was deprecated.
	// Deprecated: USE `TrustedPlatform` WITH VALUE `gin.PlatformGoogleAppEngine` INSTEAD
	// #726 #755 If enabled, it will trust some headers starting with
	// 'X-AppEngine...' for better integration with that PaaS.
	AppEngine bool

    // UseRawPatch如果启用,那么url.RawPath会用来找到参数
	UseRawPath bool

	// UnescapePathValues 启用时,路径值将取消转义。
	// 如果UseRawPath为false(默认情况下),UnescapePathValues实际上为true,
	// 因为url.Path将用于搜索参数,该路径已经被取消转义。
	UnescapePathValues bool

	// RemoveExtraSlash启用时,URL中的参数可以解析,即使存在多行斜杠。
	// 请参阅PR#1817和问题#1644
	RemoveExtraSlash bool

	// RemoteIPHeaders list of headers used to obtain the client IP when
	// `(*gin.Engine).ForwardedByClientIP` is `true` and
	// `(*gin.Context).Request.RemoteAddr` is matched by at least one of the
	// network origins of list defined by `(*gin.Engine).SetTrustedProxies()`.
	RemoteIPHeaders []string

	// TrustedPlatform if set to a constant of value gin.Platform*, trusts the headers set by
	// that platform, for example to determine the client IP
	TrustedPlatform string

	// MaxMultipartMemory value of 'maxMemory' param that is given to http.Request's ParseMultipartForm
	// method call.
	MaxMultipartMemory int64

	// UseH2C enable h2c support.
	UseH2C bool

	// ContextWithFallback enable fallback Context.Deadline(), Context.Done(), Context.Err() and Context.Value() when Context.Request.Context() is not nil.
	ContextWithFallback bool

	delims           render.Delims
	secureJSONPrefix string
	HTMLRender       render.HTMLRender
	FuncMap          template.FuncMap
	allNoRoute       HandlersChain
	allNoMethod      HandlersChain
	noRoute          HandlersChain
	noMethod         HandlersChain
	pool             sync.Pool
	trees            methodTrees
	maxParams        uint16
	maxSections      uint16
	trustedProxies   []string
	trustedCIDRs     []*net.IPNet
}

Tree

Gin框架重写的HTTP路由系统,采用Radix树,Radix树是前缀树的升级版本,前缀树本身会把字符串每个字符化为一个树节点,然后根据是否有公共前缀进行树的分支。Radix树把公共前缀直接纳入一个节点,减少匹配次数

RouterGroup

1
2
3
4
5
6
type RouterGroup struct {
	Handlers HandlersChain
	basePath string
	engine   *Engine
	root     bool
}

RouterGroup是Gin中用来管理路由的一个组,通常使用Group方法进行创建Group,Group函数可以不停的嵌套,比如api这个是根路由,apiv1.v2两个版本,那么我们可以新建一个api的路由组,然后对api路由组继续添加子路由组v1v2,子路由组会继承父路由组的根路由。主要涉及方法为calculateAbsolutePath此函数功能是对当前Group的basePath进行判断和拼接。

HandlerChain

处理链我认为是一个非常好的抽象,可以通过处理链来实现对数据的流水线处理,我们可以把所有的处理函数按顺序注册到处理链来实现对每个环节处理抽象。比如一个请求的处理需要以下步骤:

  1. 读取参数

  2. 参数处理

  3. 响应返回

那么通过处理链,我们可以定义三个函数按顺序注册到处理链中,每一个请求到来后都会依次执行三个步骤。

Context

 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
type Context struct {
	writermem responseWriter //响应写入器
	Request   *http.Request //请求读取器
	Writer    ResponseWriter //响应写入器接口

	Params   Params
	handlers HandlersChain //处理链
	index    int8 //索引
	fullPath string //全路径

	engine       *Engine
	params       *Params //参数列表
	skippedNodes *[]skippedNode

	// This mutex protects Keys map.
	mu sync.RWMutex

	// Keys is a key/value pair exclusively for the context of each request.
	Keys map[string]any

	// Errors is a list of errors attached to all the handlers/middlewares who used this context.
	Errors errorMsgs

	// Accepted defines a list of manually accepted formats for content negotiation.
	Accepted []string

	// queryCache caches the query result from c.Request.URL.Query().
	queryCache url.Values

	// formCache caches c.Request.PostForm, which contains the parsed form data from POST, PATCH,
	// or PUT body parameters.
	formCache url.Values

	// SameSite allows a server to define a cookie attribute making it impossible for
	// the browser to send this cookie along with cross-site requests.
	// SameSite 允许服务器定义一个cookie属性,使得浏览器不可能发送这个cookie到跨站点请求中。
	sameSite http.SameSite
}

概括:

Gin分为Gin框架与net/http两个部分,首先通过Engine对象进行服务器启动的配置,然后进入net/http的loop环节监听请求。在搭建服务器时,使用RouterGroup进行路由的管理,底层数据结构是Radix树,同时对于HandlerChain进行注册。在底层Loop过程中,所有的请求数据流都会通过注册给标准库的ServeHttp()函数处理,请求字节流被打包然后交互给Context,Context内可以直接调用标准库封装好的Request对象,然后找到RouterGroup中对应请求的HandlerChain进行依次调用,调用完成后根据用户代码写入到ResponseWriter提交给标准库,由标准库进行返回。

TLS链接与TCP

TCP

握手与挥手:客户端发起-服务端收到-客户端要发送了-服务端准备好要发送了-客户端开始发送了-服务端开始接收了

滑动窗口:一个限速队列,用于tcp流量发送的限制,长度由短变长,直到服务端返回有点处理不过来时,窗口尝试维持在一个兼顾速度和流量的状态

数据同步

单redis单mysql

常见于redis与mysql之间的数据同步

  1. 强一致性 如果要实现强一致性,必须牺牲延时,保证redis与mysql之间的数据同步不受打断
  2. 最终一致性 非强一致性情况下,如用户尝试更改用户名,不需要强一致性,保持最终一致性,有请求之间写入DB,然后废弃redis的key,等再次访问时缓存无数据从数据库重新读。

redis与mysql集群

mysql集群与单mysql主要考虑是mysql之间的数据同步,此时的强一致性需要分布式事务,最终一致性需要写入DB后,DB之间同步数据,然后配置redis的相关key过期,若DB未同步完成,可能读取到脏数据。

操作系统:

死锁:

  1. 两个线程A,B。A持有B需要的对象b,B持有A需要的对象a,A需要a等待B释放a,但是B的释放需要A的对象b,两人互不相让。

CPU高占用:

  1. 死循环:如果一个死循环不是阻塞的,那么按道理一个死循环就能消耗一个核心,尤其是计算密集型任务
  2. 死锁:死锁定义参照上面。死锁本身应为资源持有问题导致自旋且无法退出,造成CPU核心高占用
  3. 过度分页:内存不足,使用磁盘交互,而磁盘IO性能不佳频繁分页,引起过多CPU上下文切换,造成CPU利用率过高,利用率均在上下文切换上,没有处理实际任务。

磁盘分页