2019年10月

一、问题回顾

问题现象:线上业务,某个进程被卡住了,所有任务都不响应,导致业务中断。

问题原因:程序中调用了system命令,执行了一次pidof命令,然而作者万万没想到这个pidof命令会卡住了,导致整个进程都阻塞了。

排查过程

第一步,确定进程状态,看看进程在干什么:先通过ps命令得到进程的pid,然后执行strace -p ${pid}挂进去。

执行完strace后发现进程一直卡在wait调用上,但是因为程序卡住了,没有更多的信息可以输出了,也不知道程序执行到哪了,所以并不能知道为什么会执行wait卡住。

于是分析执行wait的场景:wait只会出现在创建了子进程、父进程在等待子进程退出的情况下。会不会是某个子进程卡住了导致父进程阻塞呢?这个是很有可能的,但是反复检查代码,没有发现有创建子进程的地方,并且程序就是单进程设计的,没有主动调用wait的场景。所以这一点上被排除了。

那还会是什么原因导致程序卡在wait了?会不会是strace程序分析有问题导致的?当然这种可能性很小,基本可以排除。

然后又想,程序没有主动创建子进程,会不会是通过其他系统函数间接创建了子进程?很多系统函数都会创建子进程出来,典型的是system和popen函数,代码中是不是调用这两个函数?这很有可能!

于是,就在代码中搜索system和popen,还真找到了一处调用system的地方,但是system调用的都是系统命令:

sh -c kill -usr2 `pidof xxx`

看到命令后想到的第一个问题就是:这个会卡住吗?没听说过kill会阻塞,也没听说过pidof会阻塞啊。虽然我不太相信真的是它卡住了,但是我还是不自觉的打开了gdb。。。使用gdb -p ${pid}挂进去,直接输入bt打印出堆栈调用,映入眼帘的刚好就是这个代码所在的system调用。说明,真的就是这个system卡住了。

为了确定到底是kill卡住了还是pidof卡住了,使用ps过滤出来所有执行这个命令的进程:

可以看到,所有执行pid命令的pid是大于kill的,说明是pidof卡住了才导致kill卡住。

再仔细看,系统已经存在多个被卡住的pidof命令了,最早的可以追溯到一个月前。难以相信一个pidof命令会执行一个月。

本想着再查查为什么pidof命令会卡住,但线上业务着急恢复,执行strace和gdb没看出什么信息后就先恢复了业务。后来本地再也无法重现了,也没有找到pidof卡住的真正原因。

解决方案

  1. 去掉了system机制,kill命令直接通过signal函数来完成。
  2. 进程启动的时候,保存一份pid变量,不再执行pidof命令来获取pid。

二、总结和思考

  1. 程序中少使用system或popen调用外部命令。这点是我一直以来都提倡的,排斥使用system是因为调用system要创建子进程,要屏蔽信号,会有各种不确定的因素在里面,不如系统调用简单、安全。我认为一个好的系统,要尽量减少调用外部命令(但我们公司的传统就是喜欢各种脚本、命令调来调去,实在令人头疼)。
  2. 尽可能减少重复的复杂的逻辑。这里的体现就是pidof命令,是否有必要每次都通过外部pidof命令来获取pid?现在大部分的程序都是把pid保存到一个pidfile中,需要用的时候从文件中读取,这种方式来设计是不是会好一些?

一、关于滑动窗口协议

在TCP协议中,所有的SEQ包发送出去都必须要受到对方的ACK才认为是发送成功,如果长时间没有收到ACK回复确认,发送方需要重新发送该包。

而如果发送方每次都是发送一个包,然后等到接收方回复ACK了再发送下一个包,那么数据包的传输效率就相当低了。滑动窗口协议的作用就是为了解决这个问题。

在滑动窗口协议中,发送方可以同时发送多个数据包并可以不用等待接收方确认,这样就大大增加了网络的推图两。唯一的限制是:接收方未确认的数据包不得超过双方约定的窗口个数。

TCP包头中有一个两字节的字段表示滑动窗口的大小,因此最大的滑动窗口大小为65535,这个大小是由接收方提出的,以接收方的大小为准。

最大窗口65536并不是绝对的,TCP选项中有提供一个窗口放大的选项可以对这个数值缩放。

例如以下数据,将发送的字节从1至11进行标号,接收方提出的窗口大小为6,此时已发送的1/2/3字节数据被接收方确认,当前窗口覆盖了从第4字节到第9字节的区域。4/5/6字节是已经发送但是还未收到确认,此时发送方还可以发送的数据域只剩下三个,即7/8/9三个数据。当7/8/9发送完成后,发送方无法再继续发送数据。只有接收方再确认序号4的数据后,发送方才能继续发送数据10。此时可用的窗口将会向右延伸到10,同时左边已发送并确认的数据也会向右延伸到4。

这个过程看起来就像是一个窗口不停向右滑动,因此该协议被称为滑动窗口协议。

822287-20180227212818806-1798395637.png

滑动窗口涉及到以下几个概念:

  1. 窗口合拢:当窗口左边界向右靠近时,这种现象发生在数据被发送方确认时。
  2. 窗口张开:窗口的右边界向右移动的时候,这种现象发生在接收端处理的数据的时候。
  3. 窗口收缩:窗口右边界向左移动时,这种现象不常发生。
注意:窗口大小是由接收方通告的,通过采取慢启动和拥塞避免算法等机制来使带宽和性能取得最佳。

二、坚持定时器

2.1 坚持定时器的引入

思考以下场景:接收方的接收缓冲区已满,因此接收方发送了一个窗口大小为0的数据到发送方,此时发送方停止发送数据。经过一段时间后,接收方有空闲的缓冲区可以接收数据,此时给发送方发了一个窗口大小不为0的包,但是恰巧这个包在路上出现了意外没有到达发送方。此时发送方还在一直等待接收方发送不为0的窗口过来,但是接收方又在等待发送方继续发送数据。两者相当于发生了死锁。

2.2 工作原理

为了解决上面这个问题,TCP引入了坚持定时器,该定时器功能为:TCP连接的一方收到对方窗口为0的通知时,启动该定时器,若定时器持续时间到达后还没有收到对方窗口大小不为0的通知,主动发送一个零窗口探测包(仅携带1字节数据),对方则需要在这个包中回复当前窗口的大小。

三、关于window scale选项

window scale选项是提供给滑动窗口协议用于对窗口大小放大使用,在SYN包或者SYN,ACK包中作为选项字段,表示滑动窗口的实际大小要根据这个值来进行缩放(SYN包本身的窗口不会缩放)。

发送方和接收方的滑动窗口缩放因子大小可以不一样,两者互不干扰,但实际上窗口大小还是以接收方为准。使用wireshark抓包很容易就能看到这个缩放选项,以下是一个数据包示例。

这个包是客户端发起的三次握手数据包:

image.png

客户端设置了窗口缩放大小为256,因此后面实际计算的时候窗口大小要乘以这个缩放大小1025(使用wireshark很容日就能看到),乘以256后得到实际的窗口大小为262400

image0855a2b1457c0f0d.png

Calculated window size: wireshark根据缩放因子计算出来的实际窗口大小。

Window size scaling factor: 窗口缩放因子。

这两个参数都是wireshark自动生成的,并不是携带在TCP包中的字段。

本文内容来源于知乎问答:Cache 和 Buffer 都是缓存,主要区别是什么?,根据各回答内容整理得到。

首先整理下两者的概念:

​ cache是缓存,buffer是缓冲。两者从名字来看十分相近,功能并不一样,不仔细琢磨很容易把两者混为一谈。

区别:

  1. 缓存的主要目的是为了提速,系统把部分磁盘的内存放到缓存中来提高运行速度。关键的一点是,如果缓存丢失,并不影响磁盘数据读取,只是读写速度慢一些。
  2. 缓冲的作用恰如其名——起一个缓冲的作用。例如写文件的时候,每次写一个字节,如果每次都把这1个字节写到磁盘,严重影响运行效率。而缓冲的作用就是把这些1字节的数据存起来,到一定的数量之后统一写到磁盘。同时,和缓存不同的是,缓冲区中的数据如果丢失了,数据就会永久丢失(例如:linux系统中dmesg的实现就是一个环形缓冲区,当日志过多时,后来的日志就会刷掉原来的,导致日志信息显示不全)。

一、简介

go module公共代理仓库,代理并缓存go模块。你可以利用该代理来避免DNS污染导致的模块拉取缓慢或失败的问题,加速你的构建。

简单来说就是国内访问被墙,go get无法在线获取到仓库,代理仓库就是帮我们解决这个问题的。

二、使用方法

  1. 使用go1.11以上版本并开启go module机制
  2. 导出GOPROXY环境变量
export GOPROXY=https://xxxx.xx

三、国内优质代理仓库

3.1 七牛

export GOPROXY=https://goproxy.cn

3.2 阿里云

export GOPROXY=https://mirrors.aliyun.com/goproxy/