Z


  • 首页

  • 标签

  • 归档

我们是如何在微服务集群中找出内存泄漏的--译

发表于 2020-09-23

Detectify后端团队的博客:

Detectify的后端团队已经使用Go有一些年头了,Go是我们提供为服务所选择的语言。我们认为Go是一个令人着迷的语言,而且它 被证明可以很好地为我们所用。它提供了一系列很棒的工具,比如接下来我们将要接触到的pprof。

然而,尽管Go表现地很好,我们发现其中一个微服务有一些很像内存泄漏的行为。

这篇文章将沿着我们如何发现这个问题、我们决策背后的思考过程、需要理解的细节和修复这个问题,一步一步探索下去。

它是如何开始的

我们可以从监控系统中看到这个微服务的内存占用在逐渐累计并且不会下降,直到触发了OOM(Out of Memory) 的错误,或者我们 重启服务。

尽管Go有许多优秀的工具,但在这次调试过程中,我们想研究完整的内核转储,但是我们发现直到撰写本文时,使用pprof(包括 其他Go工具)并不能完成这个事。pprof有其局限性,但是它提供的功能依然有助于我们追寻此内存问题的根本原因。

Profiling Go with pprof

pprof是一个Go工具,用于可视化和分析profiling数据。它作用于CPU和内存的profiling,但是这里我们将不讨论CPU profiling。

在你的服务中设置pprof非常简单。你可以直接调用pprof函数,例如:pprof.WriteHeapProfile,或者你可以设置pprof http入口,我们发现后者更有趣。

对于后者,你只需要import pprof这个package,它将注册路由/debug/pprof。有了这个,你可以通过向这个endpoint发送GET请求 就能获取pprof数据,这个方式对于在容器中运行的环境非常方便。根据pprof的文档,在生产环境中使用它是安全的,因为pprof 几乎没有额外的开销。但是注意,pprof这个http endpoints不应该被公共网络访问,因为它包含了你服务的敏感数据。

以下是你需要添加到代码中的内容:

1
2
3
4
import (
"net/http"
_ "net/http/pprof"
)

之后,你应该可以在/debug/pprof端点访问不同的pprof配置文件。例如,如果要使用heap profile,则可以执行以下操作

1
curl https://myservice/debug/pprof/heap > heap.out

该工具有多个内置配置文件,例如:

  • heap: 堆中对象内存分配的采样数据.

  • goroutine: 所有goroutine的堆栈追踪.

  • allocs: 内存分配的采样数据.

  • threadcreate: 系统线程的堆栈追踪.

  • block: 导致阻塞的堆栈追踪.

  • mutex: 堆栈中的锁状态信息.

你可以在pprof.go文件中找到有关它们的更多详细信息。

我们将会花费大部分时间在heap profile上。如果在heap pprof上你无法发现任何有用的信息,可以尝试下检查其他类型的pprof。 我们也检查了goroutine profile以确保没有任何goroutine泄露和内存泄漏。

我们在找什么?

在深入调查兔子洞之前,往回走一步然后搞清楚我们到底在找什么,这个很重要。换句话说,Go中会以什么方式出现内存泄漏( 或者其他形式的内存压力)?

Go是一个自带垃圾回收的语言,这个降低了开发者管理内存的压力,但是我们仍然需要谨慎那些不会被垃圾回收的内存。

在Go中,有几种方式会导致内存泄漏。大部分是由于:

  • 创建子字符串和子切片
  • 错误使用defer语法
  • 没有关闭HTTP的response body(或者其他未关闭的资源)
  • 被遗忘的goroutine
  • 全局变量

你可从go101, vividcortex, hackernoon了解更多

现在我们对Go的内存泄漏已经有了大概的认知,此时你可能想说:”那么我就不需要任何profiling了,我可以直接看我的代码”

实际上,一个服务都会有超过10行以上的代码和许多结构体,尽管示例代码可以很明显地展示内存泄漏,但是在没有任何提示的情况 下搜索服务源代码就如同大海捞针,我们建议你在直接查看源码前使用pprof,这样你就可以找到一些问题所在的好线索。

Heap profile总是一个很好的开端,因为heap是发生内存分配的地方。堆不是发生内存分配的唯一地方,某些内存分配也会发生 在栈中,但是接下来我们不会讨论内存管理系统的内部工作原理,你可以在文章末尾找到更多相关资源。

寻找内存泄漏

万事俱备,开始调试,我们开始查看服务的heap profile:

1
curl https://services/domain-service/debug/pprof/heap > heap.out

现在我们有了heap profile,接下来我们来分析它。运行下面的命令以开启命令行:

1
go tool pprof heap.out

命令行看起来是这样:

Type部分:inuse_space表示正在使用模式,它还可以是:

  • inuse_space: 表示pprof展示的是还未释放的内存占用空间

  • inuse_objects: 表示pprof展示的是还未释放的对象数量

  • alloc_space: 表示pprof展示的是所有内存占用空间,不管是否被释放

  • alloc_objects: 表示pprof展示的是所有对象数量,不管是否被释放

如果你想改变模式,可以执行:

1
go tool pprof -<mode> heap.out

好的,现在回到提示符,最常见的命令是top,它显示了内存消耗最大的用户。这是我们得到的:

当我们看到这种情况时,我们的第一个想法就是pprof或监视系统出现了问题,因为我们稳定地看到400MB的内存消耗, 但是pprof报告的内存约为7MB。我们登录计算机检查docker stats,他们还是报告了400MB内存消耗。

pprof 怎么了?

这是其中一些术语的简要说明:

  • flat:表示由函数本身分配的内存。
  • cum:表示一个函数或者它下游调用的所有函数所分配的内存。

我们还在pprof提示符下运行了png命令,以生成调用及其内存消耗的图表。

在这一点上,要特别提到的是pprof还支持Web UI。您还可以通过运行以下命令在浏览器中查看所有数据:

1
go tool pprof -http=:8080 heap.out

通过上面的图,我们决定看一看 GetByAPEX 这个函数,因为从图表来看大量的内存压力由这个函数产生(尽管 最大是7M)。果然,我们发现了许多可能会导致内存压力的代码,比如大量使用 json.Unmarshal , 而且 还会向切片追加许多结构体。简单来说,GetByAPEX所做的就是从Elastic集群拉取一些数据,做一些转换,然后 将它们追加到切片中再返回它。

尽管如此,这不足以造成内存泄漏,它仅仅会导致内存压力,更不用说pprof中报告的7MB和我们在监控系统中看到的 相比并没有什么。

如果您正在运行Web UI,则可以转到“Source”选项卡以逐行检查带有内存消耗注释的源代码。在命令行中使用 list 命令 也可以做到这一点。它使用正则表达式作为输入,将过滤后的源码显示给你。因此,你可以在 top 显示最占用内存的关键 函数上使用 list 。

我们决定看看被分配的对象数量,这是我们得到的结果:

在看到上面的图片后,我们认为罪魁祸首是以某种方式将结构体追加到切片中,但是分析了代码后这里不可能导致内存泄漏,因为 没有其他代码去一直保持着引用这个切片,或者引用这个切片的子切片。

此时我们想到或许是这个Elastic库导致了内存泄漏,长话短说,在这里我们也没有 发现任何问题。

在pprof的范围之外会出问题吗?

我们开始认为我们应该查看完整的核心转储,或许在向Elastic集群发出请求的时候,某些连接和goroutine被hang住?所以 我们查看了goroutine profile:

1
2
curl https://services/domain-service/debug/pprof/goroutine > goroutine.out
go tool pprof goroutine.out

一切都看起来很正常,没有异常的goroutine出现。我们也使用了netstat去检查服务容器的TCP连接的数量,也没有异常信息, 所有的TCP连接都被正常的关闭。我们发现了一些 idle 状态的连接,但是它们最终也被关闭了。

此时我们不得不面对现实,即这不是内存泄漏。那个函数造成许多内存压力,而且go的垃圾回收或者运行时也在消耗内存。 我们能做的只有将那个函数优化成流式数据,而不是将结构体保存在内存中。但同时,我们对这种奇怪的现象产生了兴趣, 我们开始重新研究Go的内存管理系统。

关于Go运行时,我们尝试了两件事:使用 runtime.GC 手动出发垃圾回收,然后调用 runtime/debug 的 FreeOSMemory。

它们都不起作用,但是我们感觉我们离罪魁祸首越来越近了。 因为我们发现一些仍然是 open 状态的 关于Go内存管理的issue, 其他人也遇到了Go运行时没有释放内存给操作系统的问题。FreeOSMemory 被认为是强制释放内存,但在我们看来它并没有生效。

我们发现Go非常依赖它分配的内存,这意味着在释放内存给操作系统之前,它将持有这些内存一段时间。如果你的应用有一个 内存消耗的峰值,然后又有至少5分钟的静默期,Go将会把内存释放给操作系统。在这之前Go将持有它,防止它需要这些内存的 时候又重新向操作系统申请。

这个听起来很好,但是我们发现5分钟后内存还是没有释放。所以我们决定做一个小实验,确认问题是否出在Go的运行时。我们 将重启应用,然后运行一个脚本,它会一段时间内请求大量数据(我们称这段时间的请求量为 x ),然后在发现一个内存峰值 的时候,我们会再多运行5秒钟然后停止。接下来,我们会重新跑这个脚本(没有5秒钟的等待期),这样我们去验证Go是否会 请求更多的内存,还是仅使用它持有的内存量。这是我们得到的结果:

实际上,Go并没有要求操作系统提供更多的内存,而是在使用它之前保留的内存。问题是5分钟规则未得到遵守, 运行时从未向操作系统释放内存。

现在我们可以确定这不是内存泄漏,但是它仍然是一个不好的表现,因为这样会浪费很多内存。

我们虽然发现了罪魁祸首,但是我们仍然感到沮丧,因为这是我们无法修复的问题,这是Go的运行时方式。我们最初的想法 是到这里停止下来,然后优化服务。但是随着我们继续深入研究,在Go仓库 发现了一些讨论Go内存管理方式变更的issue:

  • runtime: mechanism for monitoring heap size
  • runtime: scavenging doesn’t reduce reported RSS on darwin, may lead to OOMs on iOS
  • runtime: provide way to disable MADV_FREE
  • runtime: Go routine and writer memory not being released

事实证明,在Go1.12中,关于运行时通知操作系统可以回收未使用的内存的方式,发生了一些变化,在Go1.12之前,运行时 在未使用的内存上发送一个 MADV_DONTNEED 信号给操作系统,操作系统立刻回收这些内存页。从Go1.12开始,信号变成了 MADV_FREE,这会告诉操作系统如果需要的话可以回收这块不使用的内存,这也意味操作系统如果没有来自其他进程的内存压力 ,它将永远不会这样做。

除非你有其他正在运行的服务并且也很消耗内存,否则RSS(常驻内存,基本上是该服务正在消耗的内存)将不会消失。

从这个Go仓库的issue上看,这个问题仅仅会出现在iOS系统,而不会在Linux上,但是我们在Linux上遇到了相同的问题。 然后我们发现在运行Go服务的时候使用GODEBUG=madvdontneed=1去强制运行时使用MADV_DONTNEED而不是 MADV_FREE。我们决定试一试!

首先,我们在/freememory中添加了一个新HTTP端点,该端点只会调用FreeOSMemory。 这样,我们可以检查它是否确实适用于新信号。这是我们得到的:

绿线(服务-a)是我们在14:13到14:14之间调用 /freememory 的线,您可以看到它实际上将几乎所有内容释放到了操作系统。 我们没有在services-b(黄线)上调用 /freememory,但是它显然遵守了5分钟规则,并且运行时最终释放了未使用的内存!

结论

要对编程语言的运行时如何运行以及它所经历的更改有所了解! Go是一门很棒的语言,提供了许多惊人的工具,例如pprof(一直以来都是正确的,并且没有显示任何内存泄漏的迹象)。 学习如何使用它并读取其输出是我们从此“错误”中学到的最有价值的技能,因此一定要检查一下!


我们一路发现的所有链接:

  • Jonathan Levison on how he used pprof to debug a memory leak.
  • [VIDEO] Memory leaks in Go and how they look like.
  • Another memory leak debugging journey!
  • How to optimize your code if it’s suffering from memory pressure, and also general tips on how the Go memory model works.
  • How the Go garbage collector works.
  • Go’s behavior on not releasing memory to the operating system.

原文

2019年度总结

发表于 2020-01-21

2019

弹指一挥间,2019已经过去了。

外面关于流感的消息越传越严重,而我选择在工位上安静的写年度总结~

有人说如何评估自己在过去一年有多少成长?如果你觉得一年前的自己是个zz,那么说明你这一年进步了很多。我当然觉得一年前 的自己不是zz,但差不多算是半个zz。

技术能力

技术上,今年在消息中间件上做了较多研究。Kafka,NSQ,Pulsar还有我们公司自研的LeviMQ我都做了比较深入的学习。

Kafka

学习Kafka主要是通过 <深入理解Kafka:核心设计与实践原理> 这本书。让我觉得印象比较深的有几点:

  1. Kafka读写高性能的实现原理
  2. 分区的设计,这一点和许多分布式系统都有类似的思想
  3. 副本同步的实现

同时Kafka也存在一些很严重的问题,比如消费者加入/离开集群时会出现一段时间消息无法消费的情况。这也是公司内部 某些场景使用NSQ的主要原因

NSQ

是我前几年就接触过原始的开源版本,以前对它的认知就停留在:使用golang编写,高性能,但是对于消息顺序性,高可用 方面没有深入研究。有赞对这个开源版本做了较大的改造,借鉴了相当多的kafka设计理念。我们公司主要是在有赞改造的版本基础上, 又加了一些定制化功能。12月份的时候在公司内部,做了一个关于nsq-rebalance的分享,算是通读了nsq的核心逻辑, 学到不少。

Pulsar

今年入职的第一个项目就是和Pulsar有关,当时因为官方的pulsar-clieng-go是使用cgo运行,所以就找了一个 非官方的pulsar-clieng-go使用。前几个月基本都在踩这个client的坑,遇到了以前项目中没遇到的情况。 比如oom,proto编码协议,内存泄漏等各种各样的问题。9月份的时候应Pulsar社区邀请,去上海做了一次关于 Pulsar实践的分享,现在想来当时还是挺紧张的~

K8s

年底的两个月基本都在忙K8s相关的工作,算是在K8s这方面做到真正的入门级别了

生活

今年和湾湾订婚啦!很庆幸能在六年前就遇到她,真希望能一直和她走下去。

半年前她选择离职,从事自由职业。说实话我当时犹豫了半天,我担心她会不会一个人在家太闷了,担心她 会不会只是一时冲动,担心她以后的发展…最终我还是选择充分支持她。年轻的时候不去尝试,难道要等老了再后悔叹息吗? 当然从现在来看,她这个选择对我最大的影响就是:我每天都能吃到她做的饭菜,导致我越来越月半!

对了,还有我和我的乒乓球。以前和小洋哥打球总是输给他,但是某天我突然功力大涨,甚至出现了2-10逆转翻盘的情况。 后面和他交手也是至少55开的胜率,真是神奇。明年乒乓球积分赛走起?

2019有很多收获,当然也有遗憾。

年初的时候报了一个吉他班,也参加了公司的乐队,只可惜后面荒废了。 家里人有些健康问题,让我只能觉得无能为力。 我自己的身体抵抗力好像也变差很多,年中有一次感冒咳嗽持续了2个月。

希望以后能坚持锻炼,少留遗憾。2020,奥利给!

golang内存泄漏排查

发表于 2020-01-20

golang内存泄漏排查

最近有一个项目主要是从pulsar中接收数据,再推送到其他topic中。在重启pulsar的时候会触发重连,同时也会出现内存泄漏。 因为这个泄漏比较缓慢,从docker stats可以发现每天泄漏10M左右的内存

主题

  • 解决内存泄漏问题
  • 理解golang-pprof的相关数据
  • 理解golang的内存分配

复盘过程

之前我一直想从内存的pprof数据中查出内存泄漏的原因,事实证明这条路线不是很好,有以下原因:

  1. heap-profiling它通过采样,比较直观的反映了内存分配情况。一般我们的注意力会放在内存占用较多的代码块, 但是内存泄漏不一定就意味着占用较多
  2. heap-profiling是采样数据, 存在不确定性。有可能某段代码循环调用了100次,但是profiling只采集了50次
  3. dave.cheney 在博客中提到一个个人观点:memory profiling对内存泄漏排查作用不大

实际上我们完全可以换个方向,从goroutine-profiling入手

从图中可以看出在经历过项目初始化 -> 消息推送 -> 停止推送后,goroutine数量比初始化时高了一些,而且一直没有降下去。

通过收集go-prometheus上报的metrics数据,可以看到goroutine数量出现了上升且不释放的情况

通过对比两个时间点的gorourtine-profiling文件,找到新增的goroutine,然后再通过堆栈信息去/debug/pprof/goroutine?debug=2 返回的结果中查询,果不其然,有些goroutine被意外阻塞了。

例如send(ctx context.Context),ack(ctx context.Context)这些函数本不该阻塞,它们之所以被阻塞住,是因为之前在做一些 封装的时候,随意传了一个不带超时时间的context对象

runtime.Memstats解读

之前一直对golang的runtime.Memstats各个字段有些模糊的认知,这次我们来彻底把它们搞清楚!

runtime.Memstats各字段的关系

具体每个字段代表什么意思,就不细说了。可以去读标准包的代码注释,或者这里 我们主要理清楚各个字段之间的关系

debug/pprof/heap?debug=1 和 debug/pprof/heap的inuse-space数据不一致

一个是61KB,另一个是5662KB

其实关键点就在runtime.MemProfileRate 在runtime/pprof/protomem.go

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
  // runtime/pprof/protomem.go

// scaleHeapSample adjusts the data from a heap Sample to
// account for its probability of appearing in the collected
// data. heap profiles are a sampling of the memory allocations
// requests in a program. We estimate the unsampled value by dividing
// each collected sample by its probability of appearing in the
// profile. heap profiles rely on a poisson process to determine
// which samples to collect, based on the desired average collection
// rate R. The probability of a sample of size S to appear in that
// profile is 1-exp(-S/R).
func scaleHeapSample(count, size, rate int64) (int64, int64) {
if count == 0 || size == 0 {
return 0, 0
}

if rate <= 1 {
// if rate==1 all samples were collected so no adjustment is needed.
// if rate<1 treat as unknown and skip scaling.
return count, size
}

avgSize := float64(size) / float64(count)
scale := 1 / (1 - math.Exp(-avgSize/float64(rate)))

return int64(float64(count) * scale), int64(float64(size) * scale)
}

在protomem中会对数据根据runtime.MemProfileRate进行一次scale

当runtime.MemProfileRate=1时,两个profiling的inuse-space就是一致的

inuse-space和runtime.Memstats.HeapAlloc字段不一致

因为inuse-space是一份采样数据,不是全量的内存分配数据。有些博客说是采样频率是1/1000而且可以配置, 这个我还没有找到具体出处。

docker-stats, /proc/pid/status, ps, top

上面几个命令都是查看进程的内存占用情况

docker-stats 反应的是物理内存占用的一部分,和/proc/pid/status中RssAnon大致相等

1
2
CONTAINER ID        NAME                CPU %               MEM USAGE / LIMIT     MEM %               NET I/O             BLOCK I/O           PIDS
ca75e73e0bfe pulsar-util-or 0.19% 42.8MiB / 1.952GiB 2.14% 1.24MB / 2.23MB 1.65MB / 0B 8

/proc/1/status

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/data # cat /proc/1/status
Name: pulsar-util
...
VmPeak: 115852 kB
VmSize: 115852 kB
VmLck: 0 kB
VmPin: 0 kB
VmHWM: 52568 kB
VmRSS: 52568 kB
RssAnon: 42584 kB
RssFile: 9984 kB
RssShmem: 0 kB
VmData: 103536 kB
VmStk: 132 kB
VmExe: 5516 kB
VmLib: 8 kB
VmPTE: 148 kB
VmPMD: 16 kB
VmSwap: 0 kB
...

ps命令

1
2
3
4
5
/data # ps -o 'pid,rss,vsz'
PID RSS VSZ
1 51m 113m
12 1040 1592
22 4 1516

top命令 S

1
2
3
4
5
6
7
8

Mem total:2047036 anon:913928 map:103176 free:192520
slab:62844 buf:31100 cache:816244 dirty:3856 write:0
Swap total:1048572 free:1011520
PID VSZ^VSZRW^ RSS (SHR) DIRTY (SHR) STACK COMMAND
1 113m 101m 59600 4 39760 0 132 ./pulsar-util
12 1596 228 1040 760 136 0 132 /bin/sh
23 1528 160 824 760 60 0 132 top

runtime.Memstats.Sys 和 top,ps命令不一致

top,ps 显示虚拟内存占用113M,runtime.Memstats.Sys却只有71M左右

目前我理解113M是进程初始化时申请的虚拟内存大小,早期版本的go程序在初始化时甚至会申请更多。 71M是当前程序实打实使用的虚拟内存

其实这个我暂时还没有找到源码或者官方解释。。。欢迎大家补充

总结

  • 程序出现内存泄漏时,先做好数据收集工作,利用prometheus+granfana收集metrics
  • heap-profiling数据并不能很好的帮助定位内存泄漏,可以优先从goroutine追查
  • 内存泄漏可以分为暂时性泄漏和永久性泄漏,其中后者十有八九是由于goroutine泄漏导致
  • 上述排查过程中对比两份profiling文件的工作,可以编写一个工具来实现,后续完善以后会分享出来

相关链接

  1. https://colobu.com/2019/08/28/go-memory-leak-i-dont-think-so/
  2. https://dave.cheney.net/high-performance-go-workshop/dotgo-paris.html
  3. https://github.com/golang/go/issues/32284
  4. https://gfw.go101.org/article/memory-leaking.html

mysql事务与锁

发表于 2019-02-20

mysql事务

什么是mysql事务

数据库操作的最小工作单元,是作为单个逻辑工作单元执行的一系列操作。

事务是一组不可再分割的操作单元

哪些存储引擎支持事务

innodb myisam

update xxx set xxx = xxx where xxx=? 有事务吗

有。通过会话层autocommit变量来控制,默认自动提交

事务的四大特性

原子性,一致性,隔离性,持久性

事务并发的三个问题

脏读:多个事务并发,导致脏读

r1 r2
update
read(脏读)
rollback

不可重复读:一个事务的前后两次读取之间,有其他事务对数据做了修改,导致两次读取数据不一致

r1 r2
read
update
commit
read (不可重复读) -

幻读

r1 r2
read (where id>1)
insert(id=2)
commit
read (where id>1)(幻读) -

解决事务并发的方案:事务隔离

通过数据库引擎的事务隔离级别

serializable(串行化)

隔离级别 并发问题
read uncommited(未提交读)
脏读
read commited(已提交读) 只读取已提交的数据,解决了脏读
不可重复读
repeatable read(可重复读) 在同一事务中多次读取同样的数据结果是一样的,解决不可重复读的问题,未解决幻读(innodb解决了幻读)
read 幻读
serializable(串行化) -

事务隔离级别的实现方案

  1. LBCC(Lock Base concurrency control) 在读取数据之前,对其加锁,阻止其他事务对数据进行修改
  2. MVCC(Multi Version concurrency control) 生成一个快照

解决脏读:在r2事务(write/rollback)开始的时候,拿到排它锁,阻塞r1的读操作

解决不可重复读: 在r1开始的时候,拿到共享锁,阻塞r2的写操作

解决幻读:使用自增主键,行锁算法(范围)走临键锁

事务隔离级别的实现细节–’读’操作

当前读

即加锁读,读取记录的最新版本,会加锁保证其他并发事务不能修改当前记录,直至获取锁的事务释放锁;

使用当前读的操作主要包括:显式加锁的读操作与插入/更新/删除等写操作,如下所示:

1
2
3
4
5
select * from table where ? lock in share mode;
select * from table where ? for update;
insert into table values (…);
update table set ? where ?;
delete from table where ?;
1
2
3
4
5
6
7
8
9
注:当Update SQL被发给MySQL后,MySQL Server会根据where条件,读取第一条满足条件的记录,然后InnoDB引擎

会将第一条记录返回,并加锁,待MySQL Server收到这条加锁的记录之后,会再发起一个Update请求,更新这条记录。

一条记录操作完成,再读取下一条记录,直至没有满足条件的记录为止。因此,Update操作内部,就包含了当前读。同理,

Delete操作也一样。Insert操作会稍微有些不同,简单来说,就是Insert操作可能会触发Unique Key的冲突检查,也

会进行一个当前读。

快照读

即不加锁读,读取记录的快照版本而非最新版本,通过MVCC实现;

InnoDB默认的RR事务隔离级别下,不显式加『lock in share mode』与『for update』的『select』操作都属于快照读,保证事务执行过程中只有第一次读之前提交的修改和自己的修改可见,其他的均不可见;

事务隔离级别的实现细节–’写’操作

1
2
3
insert into table values (…);
update table set ? where ?;
delete from table where ?;

锁

myisam 只支持表锁 innodb 支持表锁和行锁

innodb 锁类型

  • 共享锁(行锁)
  • 排它锁(行锁)
  • 意向共享锁(表锁)
  • 意向排它锁(表锁)

共享锁

多个事务获取同一条数据的共享锁。在共享锁期间,其他事务不能对数据进行写操作

排它锁

只能有一个事务对一条数据创建排它锁。其他事务不能读也不能写

在一个事务中,update,delete,insert会自动针对数据加一个排它锁

锁的算法(范围)

  • 记录锁
  • 间隙锁
  • 临键锁

不同的事务隔离级别、不同的索引类型、是否为等值查询,使用的行锁算法也会有所不同;

当等值查询,有命中行数,且有命中唯一索引(唯一索引,主键索引)的时候,使用 记录锁

当范围查询,未命中的时候,使用 间隙锁。 间隙锁只在rr事务隔离级别存在

当范围查询,且有命中行数的时候,使用 临键锁。或者理解为除了(间隙锁,记录锁以外,其他都是临键锁)

主键索引和唯一索引

主键索引和唯一索引。 主键索引是特殊的唯一索引。 主键索引不允许为null值,唯一索引允许存在一个null值

关于是否阻塞的总结

定值查询

r1 r2 是否命中相同数据 r2是否阻塞
走索引 没走索引 阻塞
走索引 走索引 是 阻塞
走索引 走索引 否 不阻塞
没走索引 走索引 阻塞
没走索引 没走索引 阻塞

Q&A

非唯一索引,等值查询命中后,会使用哪种锁算法(记录锁,间隙锁,临键锁)?

如果行锁会造成表锁(意向锁),那么行锁的意义是什么?

以不可重复读或者幻读为例,为什么同一事务不能出现两次读取不一致的问题?

因为会破坏事务的一致性

对上面概念的梳理

相关链接

mysql事务与锁 视频 mysql锁

docker-compose部署wordpress

发表于 2019-02-13

体验wordpress

使用docker-compose安装wordpress

安装docker

移除旧的版本:

$ sudo yum remove docker \
    docker-client \
    docker-client-latest \
    docker-common \
    docker-latest \
    docker-latest-logrotate \
    docker-logrotate \
    docker-selinux \
    docker-engine-selinux \
    docker-engine

安装一些必要的系统工具:

sudo yum install -y yum-utils device-mapper-persistent-data lvm2

添加软件源信息:

sudo yum-config-manager --add-repo http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo

更新 yum 缓存:

sudo yum makecache fast

安装 Docker-ce:

sudo yum -y install docker-ce

启动 Docker 后台服务

sudo systemctl start docker

安装docker-compose

需要先安装企业版linux附加包(epel)

yum -y install epel-release

安装pip

yum -y install python-pip

更新pip

pip install --upgrade pip

安装docker-compose

pip install docker-compose

docker-compose.yml

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
version: "3"
services:

db:
image: mysql:5.7
volumes:
- db_data:/var/lib/mysql
restart: always
environment:
MYSQL_ROOT_PASSWORD: wordpress
MYSQL_DATABASE: wordpress
MYSQL_USER: wordpress
MYSQL_PASSWORD: wordpress

wordpress:
depends_on:
- db
image: wordpress:latest
ports:
- "8000:80"
restart: always
environment:
WORDPRESS_DB_HOST: db:3306
WORDPRESS_DB_USER: wordpress
WORDPRESS_DB_PASSWORD: wordpress
volumes:
db_data:

遇到的坑

中间出现过一次Access denied for user ‘wordpress‘@’172.18.0.3’的错误,原因是之前有一份yml文件,设置的 MYSQL_PASSWORD和本次不一样。

网上资料上的解决方案是

1
2
docker-compose stop
docker-compose rm -v 删除之前容器的的volumes文件(一般是在/var/lib/docker/volumes)

但是我使用 docker-compose rm -v,并没有删除文件

最后我索性直接把docker卸载重装了

snowflake-分布式唯一ID生成器

发表于 2018-09-13

为什么要了解snowflake

  1. 数据库分表以后,数据库自增id无法满足全局唯一的性质

  2. uuid作为主键无法保证id递增

snowflake算法原理

首先我们需要的是一个int64的id,可以通过对这64位bit划分命名空间,分别用来表示 时间戳,机器等来实现id唯一性。

41-bit的时间可以表示(1L<<41)/(1000L360024*365)=69年的时间

10-bit机器可以分别表示1024台机器(5个bit是数据中心,5个bit的机器ID),这种5-5的划分实际上是可以自定义的。

12个自增序列号表示同一时间戳,同一机器下的自增流水号.

上面的设计保证了理论上snowflake方案的QPS约为409.6w/s(2^12*1000)。

snowflake实现

直接参考github上的一个snowflake实现,代码略有删减

var (
    // 起始时间戳 这个可以根据实际情况定义,如果项目是从2018-01-01运行,就可以设置为2018-01-01的时间戳,可以使用到2087年
    Epoch int64 = 1288834974657

    // 机器标识位
    NodeBits uint8 = 10

    // 自增序列号
    StepBits uint8 = 12

    // 机器标识最大值,实例化node时不能大于nodeMax
    nodeMax   int64 = -1 ^ (-1 << NodeBits)

    // 用于step循环的一个标识
    stepMask  int64 = -1 ^ (-1 << StepBits)

    // snowflake需要的一个参数
    timeShift uint8 = NodeBits + StepBits

    // 相当于自增序列号
    nodeShift uint8 = StepBits
)

// 生成id的节点服务
type Node struct {
    mu   sync.Mutex
    time int64 // 最近使用的时间戳
    node int64 // 机器标识
    step int64 // 自增序列
}

// 实例化一个节点服务
func NewNode(node int64) (*Node, error) {
    if node < 0 || node > nodeMax {
        return nil, errors.New("Node number must be between 0 and " + strconv.FormatInt(nodeMax, 10))
    }

    return &Node{
        time: 0,
        node: node,
        step: 0,
    }, nil
}

// 生成id
func (n *Node) Generate() ID {

    n.mu.Lock()

    now := time.Now().UnixNano() / 1000000

    if n.time == now {
        n.step = (n.step + 1) & stepMask

        // 如果某一时间戳下的自增序列用完了,则切换时间戳
        if n.step == 0 {
            for now <= n.time {
                now = time.Now().UnixNano() / 1000000
            }
        }
    } else {
        n.step = 0
    }

    n.time = now

    // snowflake算法
    r := ID((now-Epoch)<<timeShift |
        (n.node << nodeShift) |
        (n.step),
    )

    n.mu.Unlock()
    return r
}

如何保证多个snowflake节点生成的id不重复

试想一下,如果部署两个snowflake节点,初始化的时候node值都为1,那么在同一毫秒内两个请求分别打在了两个节点上,那么有可能将会获取两个同样的id(time,node,step全部相同)。所以问题的关键是保证部署的时候,初始化workId(node)值不能重复。

其中一种解决方案是,通过etcd存储node列表

  • 服务初始化时,连接etcd服务,获取指定key下的node列表 list
  • 遍历list,取出一个最小的已过期的key值作为workId返回
  • 定时刷新该key值的过期时间,如果服务挂掉,超过过期时间后该key值可能会被其他服务占用

唯一ID的其他解决方案

  • UUID
  • 数据库自增长序列或字段
  • 步长+数据库自增序列

FAQ

什么是趋势递增

趋势递增指的是同一节点下产生的id是递增的。

在多个snowflake节点的情况下,因为节点的workId不同,同一时间下获取的id有可能是多个节点产生的,此时就无法保证全局递增

如果etcd挂了怎么办?

可以在每次获取workId后,将workId存到本地。

解决时钟回退问题

时间回流的原因一般是因为服务器做时间同步,此时可能出现生成重复id的情况。

首先判断node.time是否大于当前服务器时间,如果大于说明发生了时间回流,返回错误并报警

每隔一段时间(3s)上报自身系统时间写入

参考资料

Leaf——美团点评分布式ID生成系统 分布式唯一id:snowflake算法思考

设计模式

发表于 2018-09-10

HeadFirst设计模式

花了一周时间把这本书读完了,感觉受益良多。其中有几个模式让我感觉相见恨晚,之前有几个项目场景如果能结合设计模式来实现,应该能节省很多维护的工作。还有其他几种设计模式,并没有感觉到它们的威力。另外,各个设计模式之间的差异也让我有点绕不出来,这个只能靠以后慢慢理清了。

模式是什么?

书中提到: 模式是在某情景下,针对某问题的某种解决方案。

  • 情景(情景)
  • 问题(需求)
  • 解决方案(模式)

举个例子:

情景-我要准时上班 问题-我将钥匙锁在车里了 解决方案1-打破窗户,进入车内,启动引擎,然后开车上班

设计模式是什么?

设计模式在模式的基础上,还包括了适用性,即最佳实践。

上述的例子,打破窗户虽然是一种解决方案,但是没有解决成本约束,可能不是一个很好的解决方案。

为什么要学习设计模式?

设计模式虽然不是原则,更不是法律,但是它提供了一种指导方针。

我最近几个月一直在纠结一个问题,如何才能写出优秀甚至完美的代码?之前的很多项目代码,我觉得用另外一种结构也可以实现,但是我不知道哪种选择才是最正确的,甚至我一度认为之前的代码都是垃圾代码。

了解了设计模式之后,我找到了一些答案。目前我现在认为在当前情境下,满足以下三个条件,就是优秀的代码

  • 条理清晰
  • 易于维护
  • 方便扩展

至于完美的代码,当然存在,但是只有极少情况下才会出现:需求不会再更改了。

从这个角度上来说,fmt.Pritln("Hello World")"也算是一句”完美”代码了 -_-

如何学习设计模式?

“反设计模式” vs 设计模式

没有对比就没有伤害,针对遇到的问题,思考一些”反设计模式”,然后与设计模式做对比

设计模式 vs 设计模式

同一个问题,既能抽象工厂模式能解决,也能使用模版方法解决。横向对比设计模式之间的差异

发掘开源项目中的设计模式

我在读这本书的时候,读到观察者模式会想到nsq,读到模版方法会想到beego controller的设计,读到工厂方法会想到jaeger中初始化存储组件的代码,读到go-redis客户端代码会想到命令模式。

优秀的开源项目肯定包含了大量的设计模式,以后阅读的时候可以好好品味。

写在最后

毕业两年半,来到新公司已经半年了,博客也停更半年了。接下来的2018希望能更认真的对待博客~

golang的OpenTracing和jaeger使用

发表于 2018-06-12

OpenTracing是什么?

当代分布式跟踪系统(例如,Zipkin, Dapper, HTrace, X-Trace等)旨在解决这些问题,但是他们使用不兼容的API来实现各自的应用需求。尽管这些分布式追踪系统有着相似的API语法,但各种语言的开发人员依然很难将他们各自的系统(使用不同的语言和技术)和特定的分布式追踪系统进行整合.

OpenTracing通过提供平台无关、厂商无关的API,使得开发人员能够方便的添加(或更换)追踪系统的实现。 OpenTracing提供了用于运营支撑系统的和针对特定平台的辅助程序库。

已经实现OpenTracing协议的项目有:

  • Zipkin
  • Jaeger
  • Appdash

jaeger和OpenTracing

jaeger实现了OpenTracing,而且后端存储支持memry(默认),elasticsearch,cassandra

jaeger使用

创建一个支持serve模式和client模式的web服务

// main.go
package main
import (
    "flag"
    "log"

    jaegerClientConfig "github.com/uber/jaeger-client-go/config"
)

var (
    serverPort = flag.String("port", "8000", "server port")
    // 默认为服务模式
    actorKind  = flag.String("actor", "server", "server or client")
)

const (
    server = "server"
    client = "client"
)

func main() {
    flag.Parse()

    if *actorKind != server && *actorKind != client {
        log.Fatal("Please specify '-actor server' or '-actor client'")
    }

    cfg := jaegerClientConfig.Configuration{
        Sampler: &jaegerClientConfig.SamplerConfig{
            Type:  "const",
            Param: 1.0, // sample all traces
        },
    }
    // jaeger.NewRemoteReporter(transport)
    tracer, closer, _ := cfg.New(*actorKind)
    defer closer.Close()

    if *actorKind == server {
        runServer(tracer)
        return
    }

    runClient(tracer)

    // Close the tracer to guarantee that all spans that could
    // be still buffered in memory are sent to the tracing backend
    closer.Close()
}

服务器模式

// server.go
package main

import (
    "fmt"
    "io"
    "log"
    "net/http"
    "time"

    "github.com/opentracing-contrib/go-stdlib/nethttp"
    "github.com/opentracing/opentracing-go"
)

func getTime(w http.ResponseWriter, r *http.Request) {
    log.Print("Received getTime request")
    t := time.Now()
    ts := t.Format("Mon Jan _2 15:04:05 2006")
    io.WriteString(w, fmt.Sprintf("The time is %s", ts))
}

func redirect(w http.ResponseWriter, r *http.Request) {
    http.Redirect(w, r,
        fmt.Sprintf("http://localhost:%s/gettime", *serverPort), 301)
}

func runServer(tracer opentracing.Tracer) {
    http.HandleFunc("/gettime", getTime)
    http.HandleFunc("/", redirect)
    log.Printf("Starting server on port %s", *serverPort)
    err := http.ListenAndServe(
        fmt.Sprintf(":%s", *serverPort),
        // use nethttp.Middleware to enable OpenTracing for server
        nethttp.Middleware(tracer, http.DefaultServeMux))
    if err != nil {
        log.Fatalf("Cannot start server: %s", err)
    }
}

客户端模式

// client.go
package main

import (
    "fmt"
    "io/ioutil"
    "log"
    "net/http"

    "github.com/opentracing-contrib/go-stdlib/nethttp"
    "github.com/opentracing/opentracing-go"
    "github.com/opentracing/opentracing-go/ext"
    otlog "github.com/opentracing/opentracing-go/log"
    "golang.org/x/net/context"
)

func runClient(tracer opentracing.Tracer) {
    // nethttp.Transport from go-stdlib will do the tracing
    c := &http.Client{Transport: &nethttp.Transport{}}

    // create a top-level span to represent full work of the client
    span := tracer.StartSpan(client)
    span.SetTag(string(ext.Component), client)
    defer span.Finish()
    ctx := opentracing.ContextWithSpan(context.Background(), span)

    req, err := http.NewRequest(
        "GET",
        fmt.Sprintf("http://localhost:%s/", *serverPort),
        nil,
    )
    if err != nil {
        onError(span, err)
        return
    }

    req = req.WithContext(ctx)
    // wrap the request in nethttp.TraceRequest
    req, ht := nethttp.TraceRequest(tracer, req)
    defer ht.Finish()

    res, err := c.Do(req)
    if err != nil {
        onError(span, err)
        return
    }
    defer res.Body.Close()
    body, err := ioutil.ReadAll(res.Body)
    if err != nil {
        onError(span, err)
        return
    }
    fmt.Printf("Received result: %s\n", string(body))
}

func onError(span opentracing.Span, err error) {
    // handle errors by recording them in the span
    span.SetTag(string(ext.Error), true)
    span.LogKV(otlog.Error(err))
    log.Print(err)
}

编译 go build

执行客户端请求代码 ./opentracing-go-nethttp-demo

运行jaeger的docker镜像 docker run -d -p5775:5775/udp -p16686:16686 jaegertracing/all-in-one:latest

执行客户端代码 ./opentracing-go-nethttp-demo -actor client

此时访问 http://localhost:16686就可以看到jaeger上的记录了。

但是记录好像少了点,只有jaeger-query的,client和server的信息都没记录,查了下资料发现是因为启动jaeger的时候有些端口没开放。

docker run \
-p 5775:5775/udp \
-p 16686:16686 \
-p 6831:6831/udp \
-p 6832:6832/udp \
-p 5778:5778 \
-p 14268:14268 \
jaegertracing/all-in-one:latest

封装http请求

上面的例子就是对http请求的trace

将数据存储在es中

暂时还没成功,后面会更新。这里先把目前的操作记录下来。

在本地用docker启动了elasticsearch

docker run -d -p 9200:9200 -e "http.host=0.0.0.0" -e "transport.host=127.0.0.1" docker.elastic.co/elasticsearch/elasticsearch:5.4.0

在本地用docker-compose启动jaeger

doker-compose.yaml:

jaegertracing:
image: jaegertracing/all-in-one:latest
ports:
    - "5775:5775/udp"
    - "6831:6831/udp"
    - "6832:6832/udp"
    - "5778:5778"
    - "16686:16686"
    - "14268:14268"
command:
    - "/go/bin/standalone-linux"
    - "--span-storage.type=elasticsearch"
    - "--query.static-files=/go/src/jaeger-ui-build/build/"
environment:
  - SPAN_STORAGE_TYPE=elasticsearch

报错: {“level”:”fatal”,”ts”:1528814826.4817529,”caller”:”standalone/main.go:106”,”msg”:”Failed to init storage factory”,”error”:”health check timeout: no Elasticsearch node available”,”errorVerbose”:”no Elasticsearch node available…

解决:

启动es的方式改为:

docker run -it --rm -p 9200:9200 -p 9300:9300 -e "discovery.type=single-node" -e "xpack.security.enabled=false" docker.elastic.co/elasticsearch/elasticsearch:5.4.0

启动jaeger前将宿主主机ip写入到环境变量:

export DOCKERHOST=$(ifconfig | grep -E "([0-9]{1,3}\.){3}[0-9]{1,3}" | grep -v 127.0.0.1 | awk '{ print $2 }' | cut -f2 -d: | head -n1)

修改docker-compose文件

jaegertracing:
image: jaegertracing/all-in-one:latest
ports:
    - "5775:5775/udp"
    - "6831:6831/udp"
    - "6832:6832/udp"
    - "5778:5778"
    - "16686:16686"
    - "14268:14268"
command:
    - "/go/bin/standalone-linux"
    - "--span-storage.type=elasticsearch"
    - "--query.static-files=/go/src/jaeger-ui-build/build/"
environment:
    - SPAN_STORAGE_TYPE=elasticsearch
    - ES_SERVER_URLS=http://$DOCKERHOST:9200

启动jaeger

docker-compose -f jaeger-start-docker.yaml up

大功告成

补充几个es的查询语句

  • 检查es状态 curl http://127.0.0.1:9200

  • 检查节点状态 curl http://127.0.0.1:9200/_cat/health

  • 查询某天的数据 curl http://localhost:9200/jaeger-span-2018-06-14/_search

  • 查询数据数量 curl http://localhost:9200/jaeger-span-\*/_count

相关链接

jaeger test文档

httptrace和opentracing

jaeger官方文档

golang的time.Time遇到的坑

发表于 2018-06-11

坑的起源

使用不同的时区操作

后端存储的时间戳数字

// 入库操作
now := time.Now().Unix()
fmt.Println(now) // 2018-6-11 06:00:00 +0800 CST
db.Save(now)

// 查询操作
// 前端从时间框控件筛选时间(2018-6-11),然后传至后端
startTime = 1528675200000
fmt.Println(startTime) // 2018-06-11 08:00:00 +0800 CST
// 错误出现 上面入库的数据因为是在6点(CST时区)入库,所以查询不到

后端存储的是datetime类型

t,_ := time.Parse("2006-01-02 15:04:05","2018-06-11 06:00:00") // UTC时区
db.Save(t)

docker容器时区

docker容器修改

time.UnixNano() 大于int64的最大值

这个问题,标准包中已经说明了

// UnixNano returns t as a Unix time, the number of nanoseconds elapsed
// since January 1, 1970 UTC. The result is undefined if the Unix time
// in nanoseconds cannot be represented by an int64 (a date before the year
// 1678 or after 2262). Note that this means the result of calling UnixNano
// on the zero Time is undefined.
func (t Time) UnixNano() int64 {
    return (t.unixSec())*1e9 + int64(t.nsec())
}

总结

  1. 团队统一使用一个时区
  2. 不用time.Parse(),使用time.ParseInLocation()

优雅关闭的几种实现

发表于 2018-06-06

优雅关闭

实现原理

等待进程处理完任务,关闭进程

go1.8之后标准包实现优雅关闭

package main

import (
    "context"
    "fmt"
    "log"
    "net/http"
    "os"
    "os/signal"
    "syscall"
    "time"
)

func main() {
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        time.Sleep(5 * time.Second)
        fmt.Fprintf(w, "Hello World, %v\n", time.Now())
        fmt.Println("hello:", time.Now())
    })

    s := &http.Server{
        Addr:           ":8080",
        Handler:        http.DefaultServeMux,
        ReadTimeout:    10 * time.Second,
        WriteTimeout:   10 * time.Second,
        MaxHeaderBytes: 1 << 20,
    }

    go func() {
        log.Println(s.ListenAndServe())
        log.Println("server shutdown")
    }()

    // Handle SIGINT and SIGTERM.
    ch := make(chan os.Signal)
    signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM)
    log.Println("ch:", <-ch)

    // Stop the service gracefully.
    ctx := context.Background()
    log.Println("shut:", s.Shutdown(ctx))

    log.Println("done.")
}

通过waitgroup实现

参考beego.graceful.shutdown

优雅重启

优雅关闭可以防止程序强制终止,导致的脏数据问题。但是在关闭到重启的期间,有一个真空期,用户的请求是不会被接收到的。优雅重启可以解决这类问题

实现原理

  1. fork子进程
  2. 父进程优雅关闭

通过共享listener,即socket文件,fork子进程

func forkAndRun(ln net.Listener) {
    l := ln.(*net.TCPListener)
    newFile, _ := l.File()

    cmd := exec.Command(os.Args[0], "-graceful")
    cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
    cmd.ExtraFiles = []*os.File{newFile}
    fmt.Printf("cmd:%#v", cmd)
    cmd.Run()
}

fork子进程的时候,获取ppid,关闭父进程

if graceful {
    process, err := os.FindProcess(os.Getppid())
    fmt.Println("ppid:", os.Getppid())
    if err != nil {
        log.Println(err)
        return err
    }
    err = process.Signal(syscall.SIGTERM)
    if err != nil {
        return err
    }
}

160行代码实现一个graceful server

package mygrace

import (
    "context"
    "flag"
    "fmt"
    "log"
    "net"
    "net/http"
    "os"
    "os/exec"
    "os/signal"
    "syscall"
    "time"
)

var (
    graceful bool
)

func init() {
    // 第一次启动时不要添加graceful,否则会报错
    flag.BoolVar(&graceful, "graceful", false, "is graceful")
}

type Server struct {
    ln net.Listener
    *http.Server
    Done chan bool
}

func NewServer(addr string, handler http.Handler) (srv *Server) {
    if !flag.Parsed() {
        flag.Parse()
    }
    srv = &Server{Done: make(chan bool)}
    hsrv := &http.Server{
        Addr:           addr,
        Handler:        handler,
        ReadTimeout:    10 * time.Second, // 值如果过小,会导致client端读取resp失败
        WriteTimeout:   10 * time.Second,
        MaxHeaderBytes: 1 << 20,
    }
    srv.Server = hsrv
    return
}

// 这里addr可以不用传,因为下面
func ListenAndServe(addr string, handler http.Handler) error {
    s := NewServer(addr, handler)
    err := s.ListenAndServe()
    return err
}

func (srv *Server) ListenAndServe() error {
    // 这里获取listener
    // 如果是第一次启动,是从net.Listen获取
    // 其余情况,是根据文件描述符获取
    ln, err := srv.getListener()
    if err != nil {
        log.Println("srv.getListener():", err)
        return err
    }
    srv.ln = ln

    if graceful {
        process, err := os.FindProcess(os.Getppid())
        log.Println("ppid:", os.Getppid())
        if err != nil {
            log.Println(err)
            return err
        }
        err = process.Signal(syscall.SIGTERM)
        if err != nil {
            return err
        }
    }
    log.Println(os.Getpid(), srv.Addr)
    go srv.Serve(srv.ln)
    go srv.handleSignals()
    <-srv.Done
    log.Println("srv done!!!")
    return nil
}

func (srv *Server) shutdown() error {
    ctx := context.Background()
    err := srv.Shutdown(ctx)
    srv.Done <- true
    return err
}

func (srv *Server) fork() error {

    tl := srv.ln.(*net.TCPListener)
    file, err := tl.File()
    if err != nil {
        log.Println("ln.File() err:", err)
        return err
    }

    cmd := exec.Command(os.Args[0], "-graceful")
    cmd.Stdout = os.Stdout
    cmd.Stderr = os.Stderr
    cmd.Stdin = os.Stdin
    cmd.ExtraFiles = []*os.File{file}
    err = cmd.Start()
    if err != nil {
        log.Fatalf("Restart: Failed to launch, error: %v", err)
    }
    return nil
}

func (srv *Server) handleSignals() {
    for {
        sig := make(chan os.Signal, 1)
        signal.Notify(sig, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM)

        sigMsg := <-sig
        switch sigMsg {
        case syscall.SIGHUP:
            log.Println("receive syscall.SIGHUP")
            err := srv.fork()
            if err != nil {
                log.Println("srv.fork() err:", err)
            }
        case syscall.SIGINT:
            log.Println("receive syscall.SIGINT")
            err := srv.shutdown()
            if err != nil {
                log.Println("srv.fork() err:", err)
            }
            return
        case syscall.SIGTERM:
            log.Println("receive syscall.SIGTERM")
            err := srv.shutdown()
            if err != nil {
                log.Println("srv.fork() err:", err)
            }
            return
        }
    }
    return
}

func (srv *Server) getListener() (l net.Listener, err error) {
    if graceful {
        f := os.NewFile(uintptr(3), "")
        l, err = net.FileListener(f)
        if err != nil {
            err = fmt.Errorf("net.FileListener error: %v", err)
            return
        }
    } else {
        l, err = net.Listen("tcp", srv.Addr)
        if err != nil {
            err = fmt.Errorf("net.Listen error: %v", err)
            return
        }
    }
    return
}

Q&A

  1. 在分布式的环境下,有必要优雅重启吗? 虽然分布式环境可以避免用户请求的真空期,但是还是会可能产生脏数据。

  2. 有时候出现服务端执行完成,但是客户端读取response失败? 也许可以尝试:在服务端初始化http.Server的时候,可以把ReadTimeout,WriteTimeout时间调长一点

  3. beego的grace正确使用姿势? 在配置文件中,Graceful设置为true,重启时使用kill -1 xxx来结束程序。另外第一次执行程序时不要加参数 -graceful=true,否则getListener会因为查找listener失败的,并且因为这个时候程序的ppid为1,是没有权限杀死的,beego这一块没有做错误处理。

  4. os.FindProcess(os.Getppid()) 查出来的ppid等于1? 有两种可能,1.直接调用了-graceful参数;2.find的时候父进程已经结束,该进程会转移为1下面的子进程

  5. getListener方法中,为什么根据 os.NewFile(uintptr(3), “”) 就能获取之前的socket文件? 有待研究

相关链接

Golang实现平滑重启(优雅重启)

beego的graceful实现

12
Yh Zhang

Yh Zhang

See you again

20 日志
4 标签
© 2020 Yh Zhang
由 Hexo 强力驱动
|
主题 — NexT.Gemini v5.1.4
本站访客数 人次 本站总访问量 次