golang goroutine 堆栈

参考文章:

栈相关的数据结构

// Stack describes a Go execution stack.
// The bounds of the stack are exactly [lo, hi),
// with no implicit data structures on either side.
// stack 数据结构用于表示一个 goroutine 的执行堆栈
// 堆栈的边界范围是 [lo, hi)
type stack struct {
    lo uintptr
    hi uintptr
}

type g struct {
    // Stack parameters.
    // stack describes the actual stack memory: [stack.lo, stack.hi).
    // stackguard0 is the stack pointer compared in the Go stack growth prologue.
    // It is stack.lo+StackGuard normally, but can be StackPreempt to trigger a preemption.
    // stackguard1 is the stack pointer compared in the C stack growth prologue.
    // It is stack.lo+StackGuard on g0 and gsignal stacks.
    // It is ~0 on other goroutine stacks, to trigger a call to morestackc (and crash).
    stack       stack   // offset known to runtime/cgo
    // stackguard0 用于跟函数的 sp 进行比较,通常等于 stack.lo+StackGuard,但是在需要触发抢占调度时,会被赋值为 StackPreempt
    stackguard0 uintptr // offset known to liblink
    // stackguard1 用于跟系统线程的 sp 进行比较,在 g0 和 gsignal 上等于 stack.lo+StackGuard,在其他 goroutine 上等于 ~0,用于触发栈扩容和崩溃
    stackguard1 uintptr // offset known to liblink
    ......
}

Read more...

2019-11-12

golang goroutine 的创建、调度和释放

参考文章:

本文作为笔者学习 Golang 调度器实现的总结,笔者带着如下几个问题,研究 go1.13.1 源码。

  • goroutine 是如何创建、调度、退出的?
  • 调度器是如何对 goroutine 进行上下文切换的?
  • 抢占式调度是怎么做的?
  • go 启动的时候会创建多少个 M,M 的数量在何时会增加?
  • 当 goroutine 阻塞时,关联的 P、M 会怎样?

Read more...

2019-11-05

MySQL 大表处理

背景

业务有一张表现在有1亿多条记录,表大小500G,由于对历史数据不会再访问,可以将历史数据进行归档。

如果大表的数据都是会被访问的可以考虑使用 MySQL 的分区表,但 MySQL 的分区表功能有些限制,可参考:Restrictions and Limitations on Partitioning。如果不方便使用 MySQL 的分区表的话,可以考虑在业务上实现。

归档

Percona Toolkit 提供了一个 perl 编写的归档工具:pt-archiver

如果采用归档到文件的方式,该归档工具的工作流程类似下面的伪代码:

以下流程是笔者对 pt-archiver 与 MySQL 的连接进行抓包分析出来的。

set autocommit = 0
chunk = select archive rows limit n // --limit
count = 0
for row in chunk:
    archive one row
    delete one row
    count++
    if count == txn-size: // --txn-size
        count = 0
        commit
    if chunk is empty:
        chunk = select archive rows limit n // --limit
  • –txn-size:太小会出现非常多的小事务,增加 MySQL 负载(比如当innodb_flush_log_at_trx_commit=1时,每一个事务的commit都需要进行flush操作),太大会导致事务执行时间过长,增加出现锁等待和死锁的概率。
  • –limit:在 innodb 表上,如果没有使用 –for-update 参数的话,则 select 的时候不会跟其他事务产生锁竞争。

笔者采用的命令参数如下:

# 先导出数据,不删除表里的数据
pt-archiver --charset utf8 --source h=${MYSQL_HOST},D=${MYSQL_DB},t=${MYSQL_TABLE} --user ${MYSQL_USER} --ask-pass --file '%Y-%m-%d-%D.%t' --progress 1000 --where 'created_at < "2019-01-01 00:00:00"' --txn-size 100 --limit 1000 --no-delete
# 确认导出的数据没有问题,删除表中的数据
pt-archiver --charset utf8 --source h=${MYSQL_HOST},D=${MYSQL_DB},t=${MYSQL_TABLE} --user ${MYSQL_USER} --ask-pass --progress 1000 --where 'created_at < "2019-01-01 00:00:00"' --txn-size 100 --limit 1000 --nosafe-auto-increment --purge
  • 默认情况下,pt-archiver的查找会强制使用主键索引,虽然 match_logs 表上有 created_at 字段的索引,但是我们不能使用该索引,因为它不是唯一索引,如果使用 created_at 字段的索引会导致导出来的数据少了,这跟 pt-archiver 如何获取下一页数据有关
  • 当pt-archiver适合使用主键索引时,默认情况下,不会归档最后一条记录(即自增主键最大的记录,这是出于安全考虑,因为MySQL自增列的最大值在5.7中是存储在内存中的,没有持久化到磁盘,重启后会通过 SELECT MAX(ai_col) FROM table_name FOR UPDATE; 将获取到的值+1作为下一个可用的自增值),如果归档最后一条记录没有影响,可以添加–nosafe-auto-increment参数
  • 归档完数据后,还需要执行下OPTIMIZE TABLE ${MYSQL_TABLE},以回收空间,详细原因可参考文章:MySQL ibdata 存储空间的回收

2019-10-29

golang grpc 客户端负载均衡、重试、健康检查

Go GRPC 客户端是如何管理与服务端的连接的?

grpc.ClientConn 表示一个客户端实例与服务端之间的连接,主要包含如下数据结构:

grpc.connectivityStateManager(grpc.ClientConn.csMgr) 总体的连接状态

状态类型为 connectivity.State,有如下几种状态:

  • Idle
  • Connecting
  • Ready
  • TransientFailure
  • Shutdown

grpc.ClientConn 包含了多个 grpc.addrConn(每个 grpc.addrConn 表示客户端到一个服务端的一条连接),每个 grpc.addrConn 也有自己的连接转态。

  • 当至少有一个 grpc.addrConn.state = Ready,则 grpc.ClientConn.csMgr.state = Ready
  • 当至少有一个 grpc.addrConn.state = Connecting,则 grpc.ClientConn.csMgr.state = Connecting
  • 否则 grpc.ClientConn.csMgr.state = TransientFailure

默认实现下客户端与某一个服务端(host:port)只会建立一条连接,所有 RPC 执行都会复用这条连接。 关于为何只建立一条连接可以看下这个 issue:Use multiple connections to avoid the server’s SETTINGS_MAX_CONCURRENT_STREAMS limit #11704 不过如果使用 manual.Resolver,把同一个服务地址复制多遍,也能做到与一个服务端建立多个连接。

关于 grpc.addrConn.state 的转态切换可参考设计文档:gRPC Connectivity Semantics and API


Read more...

2019-10-19

golang channel底层实现

本文内容基于go1.13.1源码。

从 golang 官方文档https://golang.org/ref/spec#Channel_types可以了解到 channel 数据类型所支持的操作,接下来我将尝试探索这些操作底层是如何实现的:

  1. channel 底层数据结构的实现?
  2. channel 是如何创建、发送、接收的?
  3. 只能发送(chan<- int))和只能接收(<-chan int)的 channel 是如何实现的?
  4. select 是怎么实现同时监听多个 channel 的?

Read more...

2019-10-15

golang slice底层实现

本文内容基于go1.13.1源码。

通过make给局部变量分配空间时,如果空间较少,则会直接在栈上分配 slice 的 header 和 数组,否则会调用runtime.makeslice

slice 结构:

type SliceHeader struct {
    Data uintptr // 指向连续内存数组
    Len  int
    Cap  int
}

Read more...

2019-10-13

golang map底层实现

本文学习参考自:map

本文内容基于go1.13.1源码。

在阅读Go map的实现代码时,最好先了解哈希表这种数据结构实现的算法思想,对理解Go map的实现会有帮助,我这里简单总结下:

  • map 内部采用的是数组存储 KV,每个数组元素可以认为是一个桶
  • key 经过哈希算法后再与 map的数组长度取模映射到某个桶中
  • 如果多个 key 映射到了相同的桶,就意味着出现了哈希冲突,解决冲突的方式有两种:开放寻址法和链表法
  • 当 KV 过多时,map 就需要扩容(因为数组是固定大小的),扩容的策略是新分配一个更大的数组,然后在插入和删除 key 的时候,将对应的桶的数据搬移到新分配的数组的桶中。这种方式把扩容所需要的 O(n) 时间开销均摊到了 O(1) 的插入和删除操作中。
  • map 中用装载因子(map中元素的个数 / map的容量)来表示空闲位置的情况。装载因子越大,说明空闲位置越少,冲突越多。

Read more...

2019-10-07