高性能网络 | 你所不知道的TIME_WAIT和CLOSE_WAIT

本文源于大家在公众号里面的留言,既然很多人都搞不清楚TIME_WAIT和CLOSE_WAIT,那么小胖哥今天还是抽个时间,统一帮大家理理概念吧。

你遇到过TIME_WAIT的问题吗?

我相信很多都遇到过这个问题。一旦有用户在喊:网络变慢了。第一件事情就是,netstat -a | grep TIME_WAIT | wc -l 一下。哎呀妈呀,几千个TIME_WAIT.

然后,做的第一件事情就是:打开Google或者Bing,输入关键词:too many time wait。一定能找到解决方案,而排在最前面或者被很多人到处转载的解决方案一定是:

打开 sysctl.conf 文件,修改以下几个参数:

  • net.ipv4.tcp_tw_recycle = 1
  • net.ipv4.tcp_tw_reuse = 1
  • net.ipv4.tcp_timestamps = 1

你也会被告知,开启tw_recylce和tw_reuse一定需要timestamps的支持,而且这些配置一般不建议开启,但是对解决TIME_WAIT很多的问题,有很好的用处。

接下来,你就直接修改了这几个参数,reload一下,发现,咦,没几分钟,TIME_WAIT的数量真的降低了,也没发现哪个用户说有问题,然后就没有然后了。

做到这一步,相信50%或者更高比例的开发就已经止步了。问题好像解决了,但是,要彻底理解并解决这个问题,可能就没这么简单,或者说,还有很长的路要走!

什么是TIME-WAIT和CLOSE-WAIT?

所谓,要解决问题,就要先理解问题。随便改两行代码,发现bug“没有了”,也不是bug真的没有了,只是隐藏在更深的地方,你没有发现,或者以你的知识水平,你无法发现而已。

大家知道,由于socket是全双工的工作模式,一个socket的关闭,是需要四次握手来完成的。

  • 主动关闭连接的一方,调用close();协议层发送FIN包
  • 被动关闭的一方收到FIN包后,协议层回复ACK;然后被动关闭的一方,进入CLOSE_WAIT状态,主动关闭的一方等待对方关闭,则进入FIN_WAIT_2状态;此时,主动关闭的一方 等待 被动关闭一方的应用程序,调用close操作
  • 被动关闭的一方在完成所有数据发送后,调用close()操作;此时,协议层发送FIN包给主动关闭的一方,等待对方的ACK,被动关闭的一方进入LAST_ACK状态
  • 主动关闭的一方收到FIN包,协议层回复ACK;此时,主动关闭连接的一方,进入TIME_WAIT状态;而被动关闭的一方,进入CLOSED状态
  • 等待2MSL时间,主动关闭的一方,结束TIME_WAIT,进入CLOSED状态

通过上面的一次socket关闭操作,你可以得出以下几点:

  1. 主动关闭连接的一方 - 也就是主动调用socket的close操作的一方,最终会进入TIME_WAIT状态
  2. 被动关闭连接的一方,有一个中间状态,即CLOSE_WAIT,因为协议层在等待上层的应用程序,主动调用close操作后才主动关闭这条连接
  3. TIME_WAIT会默认等待2MSL时间后,才最终进入CLOSED状态;
  4. 在一个连接没有进入CLOSED状态之前,这个连接是不能被重用的!

所以,这里凭你的直觉,TIME_WAIT并不可怕(not really,后面讲),CLOSE_WAIT才可怕,因为CLOSE_WAIT很多,表示说要么是你的应用程序写的有问题,没有合适的关闭socket;要么是说,你的服务器CPU处理不过来(CPU太忙)或者你的应用程序一直睡眠到其它地方(锁,或者文件I/O等等),你的应用程序获得不到合适的调度时间,造成你的程序没法真正的执行close操作。

这里又出现两个问题:

  1. 上文提到的连接重用,那连接到底是个什么概念?
  2. 协议层为什么要设计一个TIME_WAIT状态?这个状态为什么默认等待2MSL时间才会进入CLOSED

先解释清楚这两个问题,我们再来看,开头提到的几个网络配置究竟有什么用,以及TIME_WAIT的后遗症问题。

Socket连接到底是个什么概念?

大家经常提socket,那么,到底什么是一个socket?其实,socket就是一个 五元组,包括:

  1. 源IP
  2. 源端口
  3. 目的IP
  4. 目的端口
  5. 类型:TCP or UDP

这个五元组,即标识了一条可用的连接。注意,有很多人把一个socket定义成四元组,也就是 源IP:源端口 + 目的IP:目的端口,这个定义是不正确的。

例如,如果你的本地出口IP是180.172.35.150,那么你的浏览器在连接某一个Web服务器,例如百度的时候,这条socket连接的四元组可能就是:

[180.172.35.150:45678, tcp, 180.97.33.108:80]

源IP为你的出口IP地址 180.172.35.150,源端口为随机端口 45678,目的IP为百度的某一个负载均衡服务器IP 180.97.33.108,端口为HTTP标准的80端口。

如果这个时候,你再开一个浏览器,访问百度,将会产生一条新的连接:

[180.172.35.150:43678, tcp, 180.97.33.108:80]

这条新的连接的源端口为一个新的随机端口 43678。

如此来看,如果你的本机需要压测百度,那么,你最多可以创建多少个连接呢?我在文章《云思路 | 轻松构建千万级投票系统》里也稍微提过这个问题,没有阅读过本文的,可以发送“投票系统”阅读。

第二个问题,TIME_WAIT有什么用?

如果我们来做个类比的话,TIME_WAIT的出现,对应的是你的程序里的异常处理,它的出现,就是为了解决网络的丢包和网络不稳定所带来的其他问题:

第一,防止前一个连接【五元组,我们继续以 180.172.35.150:45678, tcp, 180.97.33.108:80 为例】上延迟的数据包或者丢失重传的数据包,被后面复用的连接【前一个连接关闭后,此时你再次访问百度,新的连接可能还是由180.172.35.150:45678, tcp, 180.97.33.108:80 这个五元组来表示,也就是源端口凑巧还是45678】错误的接收(异常:数据丢了,或者传输太慢了),参见下图:

  • SEQ=3的数据包丢失,重传第一次,没有得到ACK确认
  • 如果没有TIME_WAIT,或者TIME_WAIT时间非常端,那么关闭的连接【180.172.35.150:45678, tcp, 180.97.33.108:80 的状态变为了CLOSED,源端口可被再次利用】,马上被重用【对180.97.33.108:80新建的连接,复用了之前的随机端口45678】,并连续发送SEQ=1,2 的数据包
  • 此时,前面的连接上的SEQ=3的数据包再次重传,同时,seq的序号刚好也是3(这个很重要,不然,SEQ的序号对不上,就会RST掉),此时,前面一个连接上的数据被后面的一个连接错误的接收

第二,确保连接方能在时间范围内,关闭自己的连接。其实,也是因为丢包造成的,参见下图:

  • 主动关闭方关闭了连接,发送了FIN;
  • 被动关闭方回复ACK同时也执行关闭动作,发送FIN包;此时,被动关闭的一方进入LAST_ACK状态
  • 主动关闭的一方回去了ACK,主动关闭一方进入TIME_WAIT状态;
  • 但是最后的ACK丢失,被动关闭的一方还继续停留在LAST_ACK状态
  • 此时,如果没有TIME_WAIT的存在,或者说,停留在TIME_WAIT上的时间很短,则主动关闭的一方很快就进入了CLOSED状态,也即是说,如果此时新建一个连接,源随机端口如果被复用,在connect发送SYN包后,由于被动方仍认为这条连接【五元组】还在等待ACK,但是却收到了SYN,则被动方会回复RST
  • 造成主动创建连接的一方,由于收到了RST,则连接无法成功

所以,你看到了,TIME_WAIT的存在是很重要的,如果强制忽略TIME_WAIT,还是有很高的机率,造成数据粗乱,或者短暂性的连接失败。

那么,为什么说,TIME_WAIT状态会是持续2MSL(2倍的max segment lifetime)呢?这个时间可以通过修改内核参数调整吗?第一,这个2MSL,是RFC 793里定义的,参见RFC的截图标红的部分:


这个定义,更多的是一种保障(IP数据包里的TTL,即数据最多存活的跳数,真正反应的才是数据在网络上的存活时间),确保最后丢失了ACK,被动关闭的一方再次重发FIN并等待回复的ACK,一来一去两个来回。内核里,写死了这个MSL的时间为:30秒(有读者提醒,RFC里建议的MSL其实是2分钟,但是很多实现都是30秒),所以TIME_WAIT的即为1分钟:

所以,再次回想一下前面的问题,如果一条连接,即使在四次握手关闭了,由于TIME_WAIT的存在,这个连接,在1分钟之内,也无法再次被复用,那么,如果你用一台机器做压测的客户端,你一分钟能发送多少并发连接请求?如果这台是一个负载均衡服务器,一台负载均衡服务器,一分钟可以有多少个连接同时访问后端的服务器呢?

TIME_WAIT很多,可怕吗?

如果你通过 ss -tan state time-wait | wc -l 发现,系统中有很多TIME_WAIT,很多人都会紧张。多少算多呢?几百几千?如果是这个量级,其实真的没必要紧张。第一,这个量级,因为TIME_WAIT所占用的内存很少很少;因为记录和寻找可用的local port所消耗的CPU也基本可以忽略。

会占用内存吗?当然任何你可以看到的数据,内核里都需要有相关的数据结构来保存这个数据啊。一条Socket处于TIME_WAIT状态,它也是一条“存在”的socket,内核里也需要有保持它的数据:

  1. 内核里有保存所有连接的一个hash table,这个hash table里面既包含TIME_WAIT状态的连接,也包含其他状态的连接。主要用于有新的数据到来的时候,从这个hash table里快速找到这条连接。不同的内核对这个hash table的大小设置不同,你可以通过dmesg命令去找到你的内核设置的大小:
  2. 还有一个hash table用来保存所有的bound ports,主要用于可以快速的找到一个可用的端口或者随机端口:

由于内核需要保存这些数据,必然,会占用一定的内存。

会消耗CPU吗?当然!每次找到一个随机端口,还是需要遍历一遍bound ports的吧,这必然需要一些CPU时间。

TIME_WAIT很多,既占内存又消耗CPU,这也是为什么很多人,看到TIME_WAIT很多,就蠢蠢欲动的想去干掉他们。其实,如果你再进一步去研究,1万条TIME_WAIT的连接,也就多消耗1M左右的内存,对现代的很多服务器,已经不算什么了。至于CPU,能减少它当然更好,但是不至于因为1万多个hash item就担忧。

如果,你真的想去调优,还是需要搞清楚别人的调优建议,以及调优参数背后的意义!

TIME_WAIT调优,你必须理解的几个调优参数

在具体的图例之前,我们还是先解析一下相关的几个参数存在的意义。

net.ipv4.tcp_timestamps

RFC 1323 在 TCP Reliability一节里,引入了timestamp的TCP option,两个4字节的时间戳字段,其中第一个4字节字段用来保存发送该数据包的时间,第二个4字节字段用来保存最近一次接收对方发送到数据的时间。有了这两个时间字段,也就有了后续优化的余地。

tcp_tw_reuse 和 tcp_tw_recycle就依赖这些时间字段。

net.ipv4.tcp_tw_reuse

字面意思,reuse TIME_WAIT状态的连接。

时刻记住一条socket连接,就是那个五元组,出现TIME_WAIT状态的连接,一定出现在主动关闭连接的一方。所以,当主动关闭连接的一方,再次向对方发起连接请求的时候(例如,客户端关闭连接,客户端再次连接服务端,此时可以复用了;负载均衡服务器,主动关闭后端的连接,当有新的HTTP请求,负载均衡服务器再次连接后端服务器,此时也可以复用),可以复用TIME_WAIT状态的连接。

通过字面解释,以及例子说明,你看到了,tcp_tw_reuse应用的场景:某一方,需要不断的通过“短连接”连接其他服务器,总是自己先关闭连接(TIME_WAIT在自己这方),关闭后又不断的重新连接对方。

那么,当连接被复用了之后,延迟或者重发的数据包到达,新的连接怎么判断,到达的数据是属于复用后的连接,还是复用前的连接呢?那就需要依赖前面提到的两个时间字段了。复用连接后,这条连接的时间被更新为当前的时间,当延迟的数据达到,延迟数据的时间是小于新连接的时间,所以,内核可以通过时间判断出,延迟的数据可以安全的丢弃掉了。

这个配置,依赖于连接双方,同时对timestamps的支持。同时,这个配置,仅仅影响outbound连接,即做为客户端的角色,连接服务端[connect(dest_ip, dest_port)]时复用TIME_WAIT的socket。

net.ipv4.tcp_tw_recycle

字面意思,销毁掉 TIME_WAIT。

当开启了这个配置后,内核会快速的回收处于TIME_WAIT状态的socket连接。多快?不再是2MSL,而是一个RTO(retransmission timeout,数据包重传的timeout时间)的时间,这个时间根据RTT动态计算出来,但是远小于2MSL。

有了这个配置,还是需要保障 丢失重传或者延迟的数据包,不会被新的连接(注意,这里不再是复用了,而是之前处于TIME_WAIT状态的连接已经被destroy掉了,新的连接,刚好是和某一个被destroy掉的连接使用了相同的五元组而已)所错误的接收。在启用该配置,当一个socket连接进入TIME_WAIT状态后,内核里会记录包括该socket连接对应的五元组中的对方IP等在内的一些统计数据,当然也包括从该对方IP所接收到的最近的一次数据包时间。当有新的数据包到达,只要时间晚于内核记录的这个时间,数据包都会被统统的丢掉。

这个配置,依赖于连接双方对timestamps的支持。同时,这个配置,主要影响到了inbound的连接(对outbound的连接也有影响,但是不是复用),即做为服务端角色,客户端连进来,服务端主动关闭了连接,TIME_WAIT状态的socket处于服务端,服务端快速的回收该状态的连接。

由此,如果客户端处于NAT的网络(多个客户端,同一个IP出口的网络环境),如果配置了tw_recycle,就可能在一个RTO的时间内,只能有一个客户端和自己连接成功(不同的客户端发包的时间不一致,造成服务端直接把数据包丢弃掉)。

我尽量尝试用文字解释清楚,但是,来点案例和图示,应该有助于我们彻底理解。

我们来看这样一个网络情况:

  1. 客户端IP地址为:180.172.35.150,我们可以认为是浏览器
  2. 负载均衡有两个IP,外网IP地址为 115.29.253.156,内网地址为10.162.74.10;外网地址监听80端口
  3. 负载均衡背后有两台Web服务器,一台IP地址为 10.162.74.43,监听80端口;另一台为 10.162.74.44,监听 80 端口
  4. Web服务器会连接数据服务器,IP地址为 10.162.74.45,监听 3306 端口

这种简单的架构下,我们来看看,在不同的情况下,我们今天谈论的tw_reuse/tw_recycle对网络连接的影响。

先做个假定:

  1. 客户端通过HTTP/1.1连接负载均衡,也就是说,HTTP协议投Connection为keep-alive,所以我们假定,客户端 对 负载均衡服务器 的socket连接,客户端会断开连接,所以,TIME_WAIT出现在客户端
  2. Web服务器和MySQL服务器的连接,我们假定,Web服务器上的程序在连接结束的时候,调用close操作关闭socket资源连接,所以,TIME_WAIT出现在 Web 服务器端。

那么,在这种假定下:

  1. Web服务器上,肯定可以配置开启的配置:tcp_tw_reuse;如果Web服务器有很多连向DB服务器的连接,可以保证socket连接的复用。
  2. 那么,负载均衡服务器和Web服务器,谁先关闭连接,则决定了我们怎么配置tcp_tw_reuse/tcp_tw_recycle了

方案一:负载均衡服务器 首先关闭连接 

在这种情况下,因为负载均衡服务器对Web服务器的连接,TIME_WAIT大都出现在负载均衡服务器上,所以,在负载均衡服务器上的配置:

  • net.ipv4.tcp_tw_reuse = 1 //尽量复用连接
  • net.ipv4.tcp_tw_recycle = 0 //不能保证客户端不在NAT的网络啊

在Web服务器上的配置为:

  • net.ipv4.tcp_tw_reuse = 1 //这个配置主要影响的是Web服务器到DB服务器的连接复用
  • net.ipv4.tcp_tw_recycle: 设置成1和0都没有任何意义。想一想,在负载均衡和它的连接中,它是服务端,但是TIME_WAIT出现在负载均衡服务器上;它和DB的连接,它是客户端,recycle对它并没有什么影响,关键是reuse

方案二:Web服务器首先关闭来自负载均衡服务器的连接

在这种情况下,Web服务器变成TIME_WAIT的重灾区。负载均衡对Web服务器的连接,由Web服务器首先关闭连接,TIME_WAIT出现在Web服务器上;Web服务器对DB服务器的连接,由Web服务器关闭连接,TIME_WAIT也出现在它身上,此时,负载均衡服务器上的配置:

  • net.ipv4.tcp_tw_reuse:0 或者 1 都行,都没有实际意义
  • net.ipv4.tcp_tw_recycle=0 //一定是关闭recycle

在Web服务器上的配置:

  • net.ipv4.tcp_tw_reuse = 1 //这个配置主要影响的是Web服务器到DB服务器的连接复用
  • net.ipv4.tcp_tw_recycle=1 //由于在负载均衡和Web服务器之间并没有NAT的网络,可以考虑开启recycle,加速由于负载均衡和Web服务器之间的连接造成的大量TIME_WAIT

回答几个大家提到的几个问题

  1. 请问我们所说连接池可以复用连接,是不是意味着,需要等到上个连接time wait结束后才能再次使用?

所谓连接池复用,复用的一定是活跃的连接,所谓活跃,第一表明连接池里的连接都是ESTABLISHED的,第二,连接池做为上层应用,会有定时的心跳去保持连接的活跃性。既然连接都是活跃的,那就不存在有TIME_WAIT的概念了,在上篇里也有提到,TIME_WAIT是在主动关闭连接的一方,在关闭连接后才进入的状态。既然已经关闭了,那么这条连接肯定已经不在连接池里面了,即被连接池释放了。

2. 想请问下,作为负载均衡的机器随机端口使用完的情况下大量time_wait,不调整你文字里说的那三个参数,有其他的更好的方案吗?

第一,随机端口使用完,你可以通过调整/etc/sysctl.conf下的net.ipv4.ip_local_port_range配置,至少修改成 net.ipv4.ip_local_port_range=1024 65535,保证你的负载均衡服务器至少可以使用6万个随机端口,也即可以有6万的反向代理到后端的连接,可以支持每秒1000的并发(想一想,因为TIME_WAIT状态会持续1分钟后消失,所以一分钟最多有6万,每秒1000);如果这么多端口都使用完了,也证明你应该加服务器了,或者,你的负载均衡服务器需要配置多个IP地址,或者,你的后端服务器需要监听更多的端口和配置更多的IP(想一下socket的五元组)

第二,大量的TIME_WAIT,多大量?如果是几千个,其实不用担心,因为这个内存和CPU的消耗有一些,但是是可以忽略的。

第三,如果真的量很大,上万上万的那种,可以考虑,让后端的服务器主动关闭连接,如果后端服务器没有外网的连接只有负载均衡服务器的连接(主要是没有NAT网络的连接),可以在后端服务器上配置tw_recycle,然后同时,在负载均衡服务器上,配置tw_reuse。

  1. 如果想深入的学习一下网络方面的知识,有什么推荐的?

学习网络比学一门编程语言“难”很多。所谓难,其实,是因为需要花很多的时间投入。我自己不算精通,只能说入门和理解。基本书可以推荐:《TCP/IP 协议详解》,必读;《TCP/IP高效编程:改善网络程序的44个技巧》,必读;《Unix环境高级编程》,必读;《Unix网络编程:卷一》,我只读过卷一;另外,还需要熟悉一下网络工具,tcpdump以及wireshark,我的notes里有一个一站式学习Wireshark:https://github.com/dafang/notebook/issues/114,也值得一读。有了这些积累,可能就是一些实践以及碎片化的学习和积累了。

写在最后

这篇文章我断断续续写了两天,内容找了多个地方去验证,包括看到Vincent Bernat的一篇文章以及Vincent在多个地方和别人的讨论。期间,我也花了一些时间和Vincent探讨了几个我没在tcp源码里翻找到的有疑问的地方。

我力求比散布在网上的文章做到准确并尽量整理的清晰一些。但是,也难免会

有疏漏或者有错误的地方,高手看到可以随时指正,并和我讨论,大家一起研究!

感谢您阅读。

Golang 二值化图片,并读取某个色块的坐标

package main

import (
"fmt"
"github.com/Comdex/imgo"
"os"
"strings"
)

func main() {
if len(os.Args) <= 1 {
fmt.Println(`需要指定一个本地的图片文件`)
return
}

img := imgo.MustRead(os.Args[1])
text := ""

for i := 0; i < len(img); i++ {
w := img[i]
for ii := 0; ii < len(w); ii++ {
//根据色块的 RGBA 色值,进行灰度二值化
//纯绿色块是我们要标记的值
r := "0"
if w[ii][0] > 125 {
r = "1"
}

text += r
}

text += "\n"
}

//过滤杂色
text = strings.Replace(text, "10", "11", -1)
text = strings.Replace(text, "01", "11", -1)

if len(os.Args) >= 3 {
fmt.Println(text)
}

x := 50
y := 50
result := "fail"

newImg := strings.Split(text, "\n")
for i := 0; i < len(newImg); i++ {
//读取绿色块的连续值
//如果做得严格一点,需要做转置
l := len(newImg[i])
if l > 0 && strings.Count(newImg[i], "0")*4 >= len(newImg[i]) {
//不能位于边界
if i+6 > l {
y = i + 3
} else {
y = i + 6 //字符高度不变,就写死增加两个像素的偏移
}

//查找连续色块
block := "00000000"

s := strings.IndexAny(newImg[i], block)
e := strings.LastIndexAny(newImg[i], block)

x = s + (e-s+4)/2

result = "ok"
break
}
}

fmt.Printf(`{"result":"%s", "x":"%d", "y":"%d"}`, result, x, y)
fmt.Println()
}

go caddy server install on Centos 6 and init.d config

# step 1, install caddyserver
curl -s https://getcaddy.com | bash
groupadd -g 33 www-data
useradd \
  -g www-data --no-user-group \
  --home-dir /var/www --no-create-home \
  --shell /usr/sbin/nologin \
  --system --uid 33 www-data

mkdir /etc/caddy
chown -R root:www-data /etc/caddy
mkdir /etc/ssl/caddy
chown -R www-data:root /etc/ssl/caddy
chmod 0770 /etc/ssl/caddy

# step 2, download sysvinit file
wget https://raw.githubusercontent.com/mholt/caddy/master/dist/init/linux-sysvinit/caddy -O /etc/init.d/caddy

# step 3, install daemon
cd /usr/local/src
wget http://developer.axis.com/download/distribution/apps-sys-utils-start-stop-daemon-IR1_9_18-2.tar.gz
tar zxvf apps-sys-utils-start-stop-daemon-IR1_9_18-2.tar.gz
cd apps/sys-utils/start-stop-daemon-IR1_9_18-2
gcc start-stop-daemon.c -o start-stop-daemon
cp start-stop-daemon /usr/sbin/

# step 4, start service
service caddy start

MAC 解决Unsupported major.minor version 52.0问题

FAILURE: Build failed with an exception.

* Where:
Build file &#039;/Users/4wei.cn/work/Lab/panda.android/PandaAndroidDemo/build.gradle&#039; line: 1

* What went wrong:
A problem occurred evaluating project &#039;:PandaAndroidDemo&#039;.
&gt; java.lang.UnsupportedClassVersionError: com/android/build/gradle/AppPlugin : Unsupported major.minor version 52.0

* Try:
Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more log output.

BUILD FAILED

目前看,是因为mac上存在多个java版本时调用了低版本的java,或者java版本过低,解决办法是进行mac中不同jdk版本切换

查看默认java版本

javac -version

修改为使用Android Studio中带的java版本不用再重新下载新的jdk了

cat  ~/.bash_profile
export ANDROID_HOME=~/Library/Android/sdk
export JAVA_HOME=&quot;/Applications/Android Studio.app/Contents/jre/jdk/Contents/Home&quot;
export PATH=$PATH:$JAVA_HOME:$ANDROID_HOME

然后重启终端,再查看java版本

javac -version
javac 1.8.0_112-release

Mac下自动开启SSH 隧道

SSH遇到网络抖动,就会断开,影响翻墙梯的连接,找到了autossh这个神器,可以断线重连

0x0 安装autossh

brew install autossh

0x01 设置mac开机启动配置文件~/Library/LaunchAgents/homebrew.mxcl.autossh.plist

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
  <dict>
    <key>Label</key>
    <string>homebrew.mxcl.autossh</string>
    <key>ProgramArguments</key>
    <array>
    <string>/usr/local/bin/autossh</string>
    <string>-M</string>
    <string>8111</string>
    <string>-N</string>
    <string>shuhai@8.8.8.8</string>
    <string>-p</string>
    <string>22</string>
    <string>-D</string>
    <string>localhost:8032</string>
    <string>-C</string>
    <string>-i</string>
    <string>/Users/shuhai/.ssh/id_rsa</string>
    </array>
    <key>RunAtLoad</key>
    <true/>
  </dict>
</plist>

0x02

launchctl load   ~/Library/LaunchAgents/homebrew.mxcl.autossh.plist
launchctl unload ~/Library/LaunchAgents/homebrew.mxcl.autossh.plist

0x03
在翻墙工具中增加sockets5端口,如 SwitchyOmega 中配置为 SOCKETS5, 127.0.0.1, 8032

0x04
如果将plist放到 /Library/LaunchAgents/ 中,则需要给该文件增加权限

sudo chown root   /Library/LaunchAgents/homebrew.mxcl.autossh.plist
sudo schgrp wheel /Library/LaunchAgents/homebrew.mxcl.autossh.plist

golang处理无限嵌套json

{
"took": 596,
"timed_out": false,
"_shards": {
"total": 11,
"successful": 11,
"failed": 0
},
"hits": {
"total": 1121497,
"max_score": 0,
"hits": []
},
"aggregations": {
"day.raw": {
"doc_count_error_upper_bound": 0,
"sum_other_doc_count": 0,
"buckets": [
{
"key": "20170418",
"doc_count": 1121497,
"channel.raw": {
"doc_count_error_upper_bound": 0,
"sum_other_doc_count": 0,
"buckets": [
{
"key": "channel01",
"doc_count": 901649,
"acttype.raw": {
"doc_count_error_upper_bound": 0,
"sum_other_doc_count": 0,
"buckets": [
{
"key": "show",
"doc_count": 424711,
"ct": {
"value": 143760
}
},
{
"key": "click",
"doc_count": 253006,
"ct": {
"value": 114883
}
},
{
"key": "install",
"doc_count": 139527,
"ct": {
"value": 68115
}
},
{
"key": "installed",
"doc_count": 84405,
"ct": {
"value": 49037
}
}
]
}
},
{
"key": "channel02",
"doc_count": 107639,
"acttype.raw": {
"doc_count_error_upper_bound": 0,
"sum_other_doc_count": 0,
"buckets": [
{
"key": "show",
"doc_count": 50364,
"ct": {
"value": 17019
}
},
{
"key": "click",
"doc_count": 32334,
"ct": {
"value": 14123
}
},
{
"key": "install",
"doc_count": 19891,
"ct": {
"value": 9259
}
},
{
"key": "installed",
"doc_count": 5050,
"ct": {
"value": 2922
}
}
]
}
},
{
"key": "channel03",
"doc_count": 69671,
"acttype.raw": {
"doc_count_error_upper_bound": 0,
"sum_other_doc_count": 0,
"buckets": [
{
"key": "show",
"doc_count": 26617,
"ct": {
"value": 8229
}
},
{
"key": "click",
"doc_count": 22793,
"ct": {
"value": 7812
}
},
{
"key": "install",
"doc_count": 19919,
"ct": {
"value": 6165
}
},
{
"key": "installed",
"doc_count": 342,
"ct": {
"value": 290
}
}
]
}
},
{
"key": "channel04",
"doc_count": 42511,
"acttype.raw": {
"doc_count_error_upper_bound": 0,
"sum_other_doc_count": 0,
"buckets": [
{
"key": "show",
"doc_count": 22565,
"ct": {
"value": 8044
}
},
{
"key": "click",
"doc_count": 11601,
"ct": {
"value": 5890
}
},
{
"key": "install",
"doc_count": 7208,
"ct": {
"value": 3802
}
},
{
"key": "installed",
"doc_count": 1137,
"ct": {
"value": 761
}
}
]
}
},
{
"key": "channel05",
"doc_count": 27,
"acttype.raw": {
"doc_count_error_upper_bound": 0,
"sum_other_doc_count": 0,
"buckets": [
{
"key": "show",
"doc_count": 20,
"ct": {
"value": 7
}
},
{
"key": "click",
"doc_count": 7,
"ct": {
"value": 2
}
}
]
}
}
]
}
}
]
}
}
}

webp与png、jpg相互转换,Convert WEBP to JPG,Convert PNG to webp

主要使用webp-tools

#ubuntu
apt-get install webp

#centos
yum -y install libwebp-devel libwebp-tools

会生成几个工具。

cwebp → WebP encoder tool
dwebp → WebP decoder tool
vwebp → WebP file viewer
webpmux → WebP muxing tool
gif2webp → Tool for converting GIF images to WebP

# convert from webp to png
dwebp mycat.jpg -o mycat.png

# Convert from JPG to WebP
cwebp some.jpg -o target.webp

dwebp可以将webp图片转换成无损的png图片格式,有了png,则可以使用imagemagic之类的工具再转换成jpg.
cwebp可以将jpg转换成webp,将png转换成webp

青云、阿里云、腾讯云磁盘速度测试

阿里云,香港,SSD云盘

[root@ali03 ~]# time dd if=/dev/zero of=/test.dbf bs=8k count=300000
300000+0 records in
300000+0 records out
2457600000 bytes (2.5 GB) copied, 29.2894 s, 83.9 MB/s

腾讯云,新加坡,默认

[root@VM_0_2_centos ~]# time dd if=/dev/zero of=/test.dbf bs=8k count=300000
300000+0 records in
300000+0 records out
2457600000 bytes (2.5 GB) copied, 3.70669 s, 663 MB/s

real    0m3.744s
user    0m0.040s
sys 0m2.450s

青云,北京3区,默认

[root@JY11 ~]# time dd if=/dev/zero of=/test.dbf bs=8k count=300000
300000+0 records in
300000+0 records out
2457600000 bytes (2.5 GB) copied, 17.3187 s, 142 MB/s

real    0m17.365s
user    0m0.033s
sys 0m1.644s

Linux / UNIX Crontab File Location Crontab配置文件路径

一般是用crontab -e来增删计划任务,如果要导入任务,则可以通过复制文件的方式来操作

Mac OS X – /usr/lib/cron/tabs/ (user cron location /usr/lib/cron/tabs/vivek)
FreeBSD/OpenBSD/NetBSD – /var/cron/tabs/ (user cron location /var/cron/tabs/vivek)
CentOS/Red Hat/RHEL/Fedora/Scientific Linux – /var/spool/cron/ (user cron location /var/spool/cron/vivek)
Debian / Ubuntu Linux – /var/spool/cron/crontabs/ (user cron location /var/spool/cron/crontabs/vivek)
HP-UX Unix – /var/spool/cron/crontabs/ (user cron location /var/spool/cron/crontabs/vivek)
IBM AIX Unix – /var/spool/cron/ (user cron location /var/spool/cron/vivek)

国内云平台价格对比(青云、阿里云、腾讯云),截至 2016-07-13

这两天没事,对比了一下国内几大云平台的成本,对比下主机、宽带、LB、CDN、备份等,先从主机开始吧。

QQ20160713-0

产品类型、规格                   青云                   腾讯云                   阿里云                   备注

北京:1核1G                       72元/月              45/月                     82/月                      腾讯云,买10个月送2个月,阿里云特惠买9个月送3个月

香港:1核1G                       115元/月            无货                       90/月                      阿里云默认送40G硬盘,其它云送20G

北京:4核8G                       500元/月            400/月                   427/月                    腾讯云,买10个月送2个月,阿里云特惠买9个月送3个月

香港:4核8G                       712元/月            400/月                   568/月                    

北京:1M带宽                     23元/月              20/月                     23/月                      腾讯云,买10个月送2个月,阿里云特惠买9个月送3个月

香港:1M带宽                     27元/月              20/月                     30/月                    

北京:10M带宽                   622元/月            565/月                   525/月                    腾讯云,买10个月送2个月,阿里云特惠买9个月送3个月

香港:10M带宽                   778元/月            565/月                   650/月                    

缓存穿透、并发和失效、同步中断,最佳实践及优化方案

原文摘自:

缓存穿透、并发和失效,来自一线架构师的解决方案
https://community.qingcloud.com/topic/463

在我们的实践中,原文中有部分解决方案已经过时,在原文的基础上,添加了几个我们常用的方案。
by shuhai, admin@4wei.cn


我们在用缓存的时候,不管是Redis或者Memcached,基本上会通用遇到以下三个问题:

  • 缓存穿透
  • 缓存并发
  • 缓存失效
  • 同步、复制中断

##缓存穿透

![https://pek3a.qingstor.com/community/resource/pic/2016-6-23-cache-1.png][1]
![https://pek3a.qingstor.com/community/resource/pic/2016-6-23-cache-2.png][2]
![https://pek3a.qingstor.com/community/resource/pic/2016-6-23-cache-3.png][3]

注:上面三个图会有什么问题呢?

我们在项目中使用缓存通常都是先检查缓存中是否存在,如果存在直接返回缓存内容,如果不存在就直接查询数据库然后再缓存查询结果返回。

这个时候如果我们查询的某一个数据在缓存中一直不存在,就会造成每一次请求都查询DB,这样缓存就失去了意义,在流量大时,可能DB就挂掉了。

那这种问题有什么好办法解决呢?

要是有人利用不存在的key频繁攻击我们的应用,这就是漏洞。有一个比较巧妙的作法是,可以将这个不存在的key预先设定一个值。比如,"key" , “&&”。

在返回这个&&值的时候,我们的应用就可以认为这是不存在的key,那我们的应用就可以决定是否继续等待继续访问,还是放弃掉这次操作。如果继续等待访问,过一个时间轮询点后,再次请求这个key,如果取到的值不再是&&,则可以认为这时候key有值了,从而避免了透传到数据库,从而把大量的类似请求挡在了缓存之中。

你应该注意,这里缓存未命中的原因,更值得我们关注。

当缓存空间满了,同步失败,网络阻塞,缓存写失败等原因,会出现缓存服务器上并没有这个key。
或者因为同步中断,在主从架构中,写到主却未同步到从的悲剧,就会出现请求穿透到DB层的情况。

出现这样的情况,一定不能直接将请求穿透到DB层,避免DB当机影响其它业务。
我们的解决方案可以参考。

  • 当业务中请求量特别高,缓存未命中的情况,应该在建立DB保护的基础上,放弃一定比例的请求,直接返回空
  • 可以随机释放一些请求到DB,控制好流量的话,能保证缓存重建且DB不受极端压力
  • 后端异步定时检查缓存,主动建立这些缓存
  • 通过建立二级缓存,把之前成功获取的缓存数据放到本机缓存,文件也好,共享内存也好,接受一些过期数据

##缓存并发

有时候如果网站并发访问高,一个缓存如果失效,可能出现多个进程同时查询DB,同时设置缓存的情况,如果并发确实很大,这也可能造成DB压力过大,还有缓存频繁更新的问题。
我现在的想法是对缓存查询加锁,如果KEY不存在,就加锁,然后查DB入缓存,然后解锁;其他进程如果发现有锁就等待,然后等解锁后返回数据或者进入DB查询。

这种情况和刚才说的预先设定值问题有些类似,只不过利用锁的方式,会造成部分请求等待。

##缓存失效

引起这个问题的主要原因还是高并发的时候,平时我们设定一个缓存的过期时间时,可能有一些会设置1分钟啊,5分钟这些,并发很高时可能会出在某一个时间同时生成了很多的缓存,并且过期时间都一样,这个时候就可能引发一当过期时间到后,这些缓存同时失效,请求全部转发到DB,DB可能会压力过重。

那如何解决这些问题呢?

其中的一个简单方案就时讲缓存失效时间分散开,比如我们可以在原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。

我们讨论的第二个问题时针对同一个缓存,第三个问题时针对很多缓存。

接下来我们将发表一些自己的缓存高可用实践,如《基于云平台的缓存集群高可用实践》,欢迎关注。

总结
1、缓存穿透:查询一个必然不存在的数据。比如文章表,查询一个不存在的id,每次都会访问DB,如果有人恶意破坏,很可能直接对DB造成影响。
2、缓存失效:如果缓存集中在一段时间内失效,DB的压力凸显。这个没有完美解决办法,但可以分析用户行为,尽量让失效时间点均匀分布。

当发生大量的缓存穿透,例如对某个失效的缓存的大并发访问就造成了缓存雪崩。

精彩问答
问题:如何解决DB和缓存一致性问题?

当修改了数据库后,有没有及时修改缓存。这种问题,以前有过实践,修改数据库成功,而修改缓存失败的情况,最主要就是缓存服务器挂了。而因为网络问题引起的没有及时更新,可以通过重试机制来解决。而缓存服务器挂了,请求首先自然也就无法到达,从而直接访问到数据库。那么我们在修改数据库后,无法修改缓存,这时候可以将这条数据放到数据库中,同时启动一个异步任务定时去检测缓存服务器是否连接成功,一旦连接成功则从数据库中按顺序取出修改数据,依次进行缓存最新值的修改。

问题:问下缓存穿透那块!例如,一个用户查询文章,通过ID查询,按照之前说的,是将缓存的KEY预先设置一个值,,如果通过ID插过来,发现是预先设定的一个值,比如说是“&&”,那之后的继续等待访问是什么意思,这个ID什么时候会真正被附上用户所需要的值呢?

我刚说的主要是咱们常用的后面配置,前台获取的场景。前台无法获取相应的key,则等待,或者放弃。当在后台配置界面上配置了相关key和value之后,那么以前的key &&也自然会被替换掉。你说的那种情况,自然也应该会有一个进程会在某一个时刻,在缓存中设置这个ID,再有新的请求到达的时候,就会获取到最新的ID和value。

问题:其实用Redis的话,那天看到一个不错的例子,双key,有一个当时生成的一个附属key来标识数据修改到期时间,然后快到的时候去重新加载数据,如果觉得key多可以把结束时间放到主key中,附属key起到锁的功能。

这种方案,之前我们实践过。这种方案会产生双份数据,而且需要同时控制附属key与key之间的关系,操作上有一定复杂度。

问题:多级缓存是什么概念呢?

多级缓存就像我今天之前给大家发的文章里面提到了,将Ehcache与Redis做二级缓存,就像我之前写的文章 http://www.jianshu.com/p/2cd6ad416a5a 提到过的。但同样会存在一致性问题,如果我们需要强一致性的话,缓存与数据库同步是会存在时间差的,所以我们在具体开发的过程中,一定要根据场景来具体分析,二级缓存更多的解决是,缓存穿透与程序的健壮性,当集中式缓存出现问题的时候,我们的应用能够继续运行。

ThinkPHP 3.2 性能优化,实现高性能API开发

需求分析

目前的业务全站使用ThinkPHP 3.2.3,前台、后台、Cli、Api等。目前的业务API访问量数千万,后端7台PHP 5.6,平均CPU使用率20%。

测试数据

真实业务
php5.6:500 QPS
php7.0:850 QPS

真实业务中减少一次Mysql查询业务或者减少一次Redis读写
php5.6:800 QPS
php7.0:1250 QPS

目前优化的结果:
ThinkPHP可以完整的跑在缓存中;
在不需要mysql查询时,不建立mysql连接;
不读写redis时,不建立redis连接。

以上数据在开发机器使用ab获取,同时也跟其它的框架做了简单对比,性能不低于其它框架。
使用zend debugger profile 可以看到框架层的时间开销占比约24%,相对于yaf这样的C语言框架10%的性能损失,一个包含缓存和ORM的框架已经算比较好的性能了。
再次吐槽一提ThinkPHP框架就喷性能不好的人,任何一个框架拿过来多做几次数据库操作,测试性能都渣得不逼,只测试输出一个HelloWorld并什么卵用。

优化过程

0x00

在项目中早期,开发压力大,没有什么时间进行项目和架构优化。
经过测试,通过添加 mysql 长连接和redis长连接,api稳定性得到非常大提升,业务最慢响应时间从4s优化到0.5s,曲线非常平稳。
PHP-FPM单机200进程,2000Request,7台PHP后端,长连接数稳定在1700左右。

产生的问题
长连接数超过5k时,性能会下降。出现过两次Mysql Server 内存用光的情况。

0x01

经过分析,发现很多API请求,是不需要建立Mysql连接的。调整代码,Mysql的查询逻辑尽量缓存到Redis里,减少对Mysql的压力。
同时对ThinkPHP的代码逻辑进行化,调用 Model 中的方法、属性,不建立Mysql连接,只有在读写db时才建立连接。减少了非常多的资源开销。
经过上述调整,Mysql的连接从1700下降到100以内,query and read QPS从5k下降到50。

优化的ThinkPHP的代码已推送到Github:

https://github.com/vus520/thinkphp/tree/shuhai/db_link_lazzy

后续是对ThinkPHP中Mysql主从、读写分离进行深度测试,增加Mysql的读能力。

0x03

当业务都严重依赖redis时,Redis的QPS一度飙升到7k,内存占用6G左右。
为了缓解redis的读压力,生产中使用了4台Redis Standalone做了1主3从架构。
并给ThinkPHP添加Redis读写分离的支持,减少Redis的压力。

https://github.com/vus520/thinkphp/blob/shuhai/db_link_lazzy/ThinkPHP/Library/Think/Cache/Driver/Redisd.class.php

目前存在的问题
Redis的高可用运维,本身也比较复杂,遇上网络抖动等原因,Redis会出现同步失败和延迟问题。
特别是在云服务器架构的环境中,网络瓶颈和延迟问题对分布式应用有非常大的影响。
很可惜,我们目前使用的青云,目前尚不能实现Redis超高可用,也不能实现无缝扩容,私网内的网络传输性能、延迟都有很大优化空间。

后续的优化计划
对redis业务进行清理,减少不必要的请求;
压缩内容;
key:value => hash;
一主多从,每个php后端部署一个redis从,优先读本机,减少网络延迟;

0x04

API项目中,禁用ThinkPHP的Session、路由、视图、行为等,进行精简加速。
经测试,性能有30%的提升。

https://github.com/vus520/thinkphp/tree/shuhai/tiny

  • 1,去掉路由
  • 2,去掉URL调度
  • 3,去掉行为、Hook
  • 4,去掉视图
  • 5,去掉控制器的反射、空操作
  • 6,去掉Session,可实现无状态的Api

0x05

在PHP7中进行深度测试,升级到PHP7,ThinkPHP 3.2的性能会有50+%的提升

Redis 优化要点

http://www.imooc.com/article/3645
http://www.cnblogs.com/mushroom/p/4738170.html
http://linusp.github.io/2015/12/16/redis-performance-analysis.html
https://segmentfault.com/a/1190000002906345

#info 分析

## Memory
>实际缓存占用的内存和Redis自身运行所占用的内存(如元数据、lua)。
>它是由Redis使用内存分配器分配的内存,所以这个数据并没有把内存碎片浪费掉的内存给统计进去
>如果used_memory > 可用最大内存,那么操作系统开始进行内存与swap空间交换
>当 rss > used ,且两者的值相差较大时,表示存在(内部或外部的)内存碎片。
>内存碎片的比率可以通过 mem_fragmentation_ratio 的值看出。
>当 used > rss 时,表示 Redis 的部分内存被操作系统换出到交换空间了,在这种情况下,操作可能会产生明显的延迟

>used_memory:9892187056
>used_memory_human:9.21G

>从操作系统上显示已经分配的内存总量, 包括碎片
>used_memory_rss:11148713984
>used_memory_peak:11236792296
>used_memory_peak_human:10.47G
>used_memory_lua:35840

## 内存碎片率
>内存碎片率稍大于1是合理的,这个值表示内存碎片率比较低,也说明redis没有发生内存交换。
>但如果内存碎片率超过1.5,那就说明Redis消耗了实际需要物理内存的150%,其中50%是内存碎片率
>若是内存碎片率低于1的话,说明Redis内存分配超出了物理内存,操作系统正在进行内存交换。内存交换会引起非常明显的响应延迟
>mem_fragmentation_ratio:1.13
>mem_allocator:jemalloc-3.6.0

## stats
>total_commands_processed:105868 #总共处理的命令数
>instantaneous_ops_per_sec:0
>rejected_connections:0
>evicted_keys:0 #因为maxmemory限制导致key被回收删除的数量

# 性能分析

## 延迟

>redis-cli -h 127.0.0.1 -p 6379 --latency
>持续采样,结果单位是ms;
>redis-cli -h 127.0.0.1 -p 6379 -–latency-history
>间隔采样,结果单位是ms;

## bigkeys

>redis-cli -h 127.0.0.1 -p 6379 --bigkeys
>持续采样,实时输出当时得到的 value 占用空间最大的 key 值

## 慢日志

>默认配置
>slowlog-log-slower-than 10000
>slowlog-max-len 128

>获取慢日志
>slowlog get 3

## 统计

>redis-cli -h 127.0.0.1 -p 6379 info commandstats
>查看所有命令统计的快照,执行次数,所耗费的毫秒数,总时间和平均时间

## 调试

>redis-cli -h 127.0.0.1 -p 6379 monitor

## 内存占用分析

redis-rdb-tools
>pip install rdbtools
>rdb -c memory /var/redis/6379/dump.rdb > memory.csv

# 优化与禁忌

停止使用 KEYS *,如果避免不了,请使用scan命令
精简键名和键值,减小key长度,压缩value
设置 key 值的过期时间,避免长时间占用内存,缓解同步和持久化的压力
选择合适的回收策略,如果不能丢数据则建议使用 volatile-lru 策略,如果key可以自动重建则推荐allkeys-lru
业务层要考虑读写分离和主从模式
合理分配snapshot,aof,主上关闭aof和snapshot,在主从上开启snapshot和aof
如果数据不需要持久,可关闭所有的持久化方式可以获得最佳的性能以及最大的内存使用量
合理选择最优的数据结构解决实际问题,那样既可以提高效率又可以节省内存
合理使用长连接
maxmemory=8g, 不要让你的Redis所在机器物理内存使用超过实际内存总量的3/5
sysctl vm.overcommit_memory=1
关闭vm-enabled,3.0以后默认废弃

解决保存快照失败后redis无法写入的问题
>config set stop-writes-on-bgsave-error no

定期日志重写,减小aof重载时的时间开销
>auto-aof-rewrite-percentage 100
>auto-aof-rewrite-min-size 64mb

# 安全
使用普通用户启动服务,且禁止该用户登录
在可以保证内网安全的情况下,无密码性能最好
客户端可能会发送config命令,会有安全问题,建议禁用
客户端可能会发送flushall、flushdb命令,为避免误操作,建议禁用
客户端可能会发送save命令,会严重影响服务器的性能,建议禁用并使用bgsave替代

>rename-command FLUSHALL ""
rename-command FLUSHDB ""
rename-command KEYS ""
rename-command save ""

# 备份策略
主关闭aof, 开启rdb
每十分钟/10000变更生成一次rdb
每个小时保存一次最新的rdb到当前磁盘
每个小时保存一次

选择一台从节点,开启rdb,开启aof,并定期重载日志
```shell
path=/opt/data/redis/
name=dump_6380.rdb
file=$(date -d "yesterday" +"%Y%m%d%H.rdb")
/bin/mv $path/$name $path/$file
echo "move file $path/$file"

file=$(date -d "-3 days" +"%Y%m%d%H.rdb")
/bin/rm -f $path/$file
```

解决"Logstash不能识别unzip解压的文件"

step 1.

find *.zip -exec unzip -j {} \;

change to

find *.zip -exec unzip -aanXj {} \;

step 2.

#Here is a working workaround:

sudo vi /etc/init.d/logstash

#modify

LS_GROUP=logstash
#by

LS_GROUP=adm
#then

sudo /etc/init.d/logstash start

遇到一个两天都没有解决的问题。

启用新的es集群,elk都使用最新的2.0+版本,同时把以前的数据打包复制到新机器上进行解压。
配置好所有文件以后,发现logstash能正常启动却不能索引日志。

好像根本没有包含任何文件。
后来发现手工写的日志可以索引,把解压后的文件cat到新的文件中,也可以索引。

测试的时候,安装了logstash 2.0, 2.1, 2.2都不能解决。Logstash 1.5却正常。

解压的文件和自己生成的文件,用户组,用户权限,md5都完全一样,除了...

update:
logstash2.x增加了ignore_older参数,调整ignore_older参数即可,默认是86400
https://www.elastic.co/guide/en/logstash/2.4/plugins-inputs-file.html#plugins-inputs-file-ignore_older