Kubernetes 艰难之路

本文参考 Github 项目 kubernetes-the-hard-way 安装 Kubernetes 集群。

kubernetes-the-hard-way 部署了3台工作节点和3台管理节点,且管理节点不运行 Pod;CNI 网络插件使用 bridge,跨主机之间的容器通信需要人为设置宿主机路由表。

本文部署采用2台 VMware Fusion 的虚拟机,操作系统为 Ubuntu Server 18.04,一台配置为管理节点,另一台配置为工作节点,同时管理节点也为工作节点;CNI 网络插件使用 Calico;并解决无法解析外部域名问题。

总体架构图:


Read more...

2019-12-22

golang gc

参考文章:垃圾回收器

因为三色标记以及混合写屏障在 Go 中的源码实现,笔者目前尚未理解清楚,所以本文省略了该部分内容。后续有时间研究明白了再补上~

并发三色标记垃圾回收

算法思想可参考:Golang’s Real-time GC in Theory and PracticeGolang’s realtime garbage collector

然而在实际实现上却有些变化,例如网上很多文章都说在启用写屏障的情况下,新创建的对象都标记为灰色,但笔者在 go 1.13 源码中,func gcmarknewobject(obj, size, scanSize uintptr)方法被 mallocgc 调用,其注释写明了:gcmarknewobject marks a newly allocated object black. obj must not contain any non-nil pointers。

Go 运行时的垃圾回收分四个阶段:

  1. 准备阶段:STW,初始化标记任务,启用写屏障
  2. 标记阶段 GCMark:标记存活对象,并发与用户代码执行,保持只占用25%CPU
  3. 标记终止阶段 GCMarkTermination:STW,关闭写屏障
  4. 清扫阶段 GCOff:回收白色对象,并发与用户代码执行

Read more...

2019-12-22

golang memory

参考文章:

Go 设计内存分配器,一是为了统一管理内存,提前分配或一次性释放大块内存,减少与操作系统进行系统调用造成的开销,进而提高程序的运行性能;二是为垃圾回收器提供支持。

传统意义上堆内存,被 Go 运行时划分为了两个部分:

  1. Go 运行时自身所需的堆内存
  2. Go 用户态代码和 goroutine 的执行栈所需的堆内存

Read more...

2019-12-16

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