May 12

Go网络实战三篇 不指定

felix021 @ 2021-5-12 23:46 [IT » 网络] 评论(0) , 引用(0) , 阅读(2302) | Via 本站原创
Aug 20

https耗时分析 不指定

felix021 @ 2020-8-20 18:34 [IT » 网络] 评论(0) , 引用(0) , 阅读(1844) | Via 本站原创
简单记一下:

$ cat curl-format.txt
time_namelookup:      %{time_namelookup}\n
time_connect:        %{time_connect}\n
  time_appconnect:    %{time_appconnect}\n
  time_pretransfer:  %{time_pretransfer}\n
  time_starttransfer: %{time_starttransfer}\n

$ curl -w '@curl-format.txt' -o /dev/null -s -L "https://www.baidu.com"
time_namelookup:      0.004
time_connect:        0.018
  time_appconnect:    0.050
  time_pretransfer:  0.050
  time_starttransfer: 0.065



或者

$ go get github.com/davecheney/httpstat

$ httpstat https://www.baidu.com
...

  DNS Lookup  TCP Connection  TLS Handshake  Server Processing  Content Transfer
[      0ms  |          9ms  |        105ms  |            10ms  |            0ms  ]
            |                |              |                  |                  |
  namelookup:0ms            |              |                  |                  |
                      connect:10ms          |                  |                  |
                                  pretransfer:116ms            |                  |
                                                    starttransfer:126ms            |
                                                                                total:126ms 
Apr 20

TCP#2: 西厢记和西厢计划 不指定

felix021 @ 2020-4-20 17:39 [IT » 网络] 评论(0) , 引用(0) , 阅读(1834) | Via 本站原创
TCP#2: 西厢记和西厢计划

引用
自那日听琴之后,多日不见莺莺,张生害了相思病,趁红娘探病之机会,托她捎信给莺莺,莺莺回信约张生月下相会。夜晚,小姐莺莺在后花园弹琴,张生听到琴声,攀上墙头一看,是莺莺在弹琴。急欲与小姐相见,便翻墙而入,莺莺见他翻墙而入,反怪他行为下流,发誓不再见他,致使张生病情愈发严重。

《西厢记》



上篇《TCP:学得越多越不懂》发出来以后,有朋友很委婉地说:“如果能结合现实生产场景会有意义一点。”

经过深刻的反思,我决定虚心接受建议,写一点理论结合实践的内容。


# == 回忆杀 ==

曾经在猫扑和天涯冲浪的网虫应该都还记得,谷歌当时还是Goooooogle,是可以直接访问的。

但是如果想搜索一些奇怪的词汇(比如███),一点击"手气不错",浏览器马上就会显示无法访问,并且这个现象会持续几分钟。

点击在新窗口中浏览此图片

于是很多小伙伴就换到一个号称自己更懂中文的搜索引擎了。

(该爬虫当年有个广告拍得不错:https://v.qq.com/x/page/r0137s2op5j.html

作为一个曾被新自由主义(Neoliberalism)洗脑的年轻人,我在寻找“自由”的路上发现了墙的存在,也知道了这是方校长的杰作。

但是墙到底是个什么样的存在呢?



# == 防火墙 ==

我们的防火墙,其名源自《The Great Firewall of China: How to Build and Control an Alternative Version of the Internet》这本书。

虽然名字叫防火墙(Firewall,简称FW),但严格来说,(在早期)它其实是一个入侵检测系统(Instrusion Detection System,简称IDS)。

和FW不同的是,IDS是监听设备,不需要部署在链路中间,只要能把流量旁路引出供它分析即可。

点击在新窗口中浏览此图片

通过旁路分析,IDS可以在不影响现有流量的情况下部署(只要路由器/交换机上有镜像端口即可),在IDS出现异常时(例如在流量高峰IDS设备性能不足时 )也不会导致网络中断。

曾经有人发现,在流量特别大的时候,墙的检测功能有时会失效,因此推测其是旁路引流进行分析的(符合IDS的特征)。

既然是旁路的,就无法直接Drop数据包,为了达到阻断通信的目的,需要利用协议的特性来实现。


# == RST大法 ==

看了上篇《TCP:学得越多越不懂》的同学,对报文的控制位里的 RST 可能还有点印象,在遇到异常情况时,可用于通知对方重置连接(细节详见RFC 793):

引用
If the receiving TCP is in a non-synchronized state (i.e. SYN-SENT, SYN-RECEIVED), it returns to LISTEN on receiving an acceptable reset. If the TCP is in one of the synchronized states (ESTABLISHED, FIN-WAIT1, FIN-WAIT2, CLOSE-WAIT, CLOSING, LAST-ACK, TIME-WAIT), it aborts the connection and informs its user

https://tools.ietf.org/html/rfc793


有些同学可能像我一样懒得读英文原文,所以翻译一下:

* 如果连接状态处于“非连接完成”状态(例如SYN-SEND, SYN-RECEIVED),当收到reset时会将状态返回LISTEN;

* 如果TCP状态是 ESTABLISHED, FIN-WAIT-1, ..., LAST-ACK, TIME-WAIT 其中之一时,放弃连接并通知用户。

忘了上述状态含义的话,可以再回顾下这张状态流转图:

点击在新窗口中浏览此图片
(tcp连接状态图,截取自rfc 793)

这就是上篇里提到的“我们敬爱的防火墙很爱用它”的原因了:

当检测到“入侵行为”时(例如HTTP报文中出现了███)发送RST,按照RFC 793规范的TCP协议栈实现,收到RST后就应当放弃本次连接。

于是你就在浏览器上看到连接被重置(reset)了。



# == 反RST大法 ==

那么,如果我忽略RST包,不就可以不被墙欺骗吗?

实际上,用 iptables 来实现这一点很简单:

$ iptables -A INPUT -p tcp --tcp-flags RST RST -j DROP


很不幸,方校长的团队对此的解决方法也非常简单,只要向双方都发送RST包就可以了。

当然如果在服务器一端也忽略RST,就可以成功绕过墙的忽悠——据说剑桥大学有人实验验证过,确实可行。

可惜的是,用户通常没法控制服务器端忽略RST,因此这个方法的实用价值不高。

但是这个思路为西厢计划做好了铺垫。



# == 西厢计划 ==

我看到这个项目的名字的时候 ,真佩服作者的脑洞。

了解这个计划的原理之后,就更佩服作者的脑洞了。

前面说到,墙是在检测到某个关键词的时候才会发送RST包。

为了检测关键词,它需要工作在应用层(HTTP协议)。

而为了工作在应用层,它需要维护TCP连接的状态。

由于那时的设备性能比较弱(所以会出现高峰期检测失效的情况),为了提高吞吐量,方校长团队的方案是:实现一个简化的TCP栈。

RFC 793规范中定义了很多有效性检测,例如检测序列号是否有效来过滤old duplicates等,以保证通信的可靠性。

但这不是墙的需求,因此可以去掉很多规则,从而提高分析性能。

那么,如果我可以欺骗墙,这个连接已经被关闭,那么后续该连接的包就会被墙认为是网络中滞留的无效包,绕过关键词检测。

具体该怎么办呢?



# == 第一阶段 ==

上篇提到了一个细节:

引用
虽然ISN=4000,但是发送方发送的第一个包,SEQ是4001开始的,TCP协议规定 SYN 需要占一个序号(虽然SYN并不是实际传输的数据),所以前面示意图中ACK的seq是 x+1 。同样,FIN也会占用一个序号,这样可以保证FIN报文的重传和确认不会有歧义。

TCP:学得越多越不懂
https://mp.weixin.qq.com/s/xyPUEFUr_v9sSKKqlBkI7w



我们知道,在三次握手的最后一步,A本应发送一个ACK(seq=y+1)。

但如果这时候 A 发送了一个 FIN 呢?

B收到以后,由于此时连接尚未建立,会直接忽略这个包。

点击在新窗口中浏览此图片

而墙实现的TCP栈比较简陋,它认为A已经关闭了链接,因此A后续发送的包就不会再触发关键词检测。

但是注意,TCP是双向的,虽然A主动关闭连接,但是B仍然可能有数据要发送(划重点:面试题“为什么TCP断开连接需要4次”的答案),因此还需要欺骗墙说在B这侧也终止链接了。

这又该怎么办呢?



# == 第二阶段  ==

显然我们不能让服务器直接发一个FIN,否则这个连接就真完了。

幸运的是,RFC 793给了一个“梯子”:

引用
If the connection is in any non-synchronized state (LISTEN, SYN-SENT, SYN-RECEIVED), and the incoming segment acknowledges something not yet sent (the segment carries an unacceptable ACK), or ...(省略)..., a reset is sent.

Reset Generation, RFC 793 [Page 35]


翻译:如果连接处于“非连接完成”状态,收到一个无效的ACK,应当发出一个reset。

如果A在三次握手的最后一步,没有按规范要求发送ACK(seq=y+1),而是发送ACK(seq=y),那么B在收到以后就会按照协议的要求回复一个RST:

点击在新窗口中浏览此图片

这时我们可以在 A 上用“反RST大法”,忽略服务端返回的RST,这个连接就不受影响。

但是墙的TCP栈认为客户端会按照协议终止连接,于是就不再有必要检测服务端后续的报文了。


# == 大结局 ==

从此张生和崔莺莺过上了幸福的生活。

方校长的团队当然不会放任这种事情的发生,西厢计划没过多久就失效了。

随着技术的进步、性能的提升,现在墙似乎已经集成到了链路中、可以直接DROP数据包,不再需要RST大法了。

不过为了业务需要,企业可以向电信主管部门申请VPN用于正常的生产经营。

例如字节跳动,为了建设21世纪数字丝绸之路,通过技术出海,在40多个国家和地区排在应用商店总榜前列,包括韩国、印尼、马来西亚、俄罗斯、土耳其等“一带一路”沿线的主要国家。

如果你也想过上幸福的生活,不妨投个简历,一起为一带一路做贡献吧。

关于字节跳动面试的详情,可参考我之前写的《程序员面试指北:面试官视角》

https://mp.weixin.qq.com/s/Byvu-w7kyby-L7FBCE24Uw

# ~ 投递链接 ~

网盟广告(穿山甲)-后端开发(上海)
https://job.toutiao.com/s/sBAvKe

网盟广告(穿山甲)-后端开发(北京)
https://job.toutiao.com/s/sBMyxk

网盟广告(穿山甲)-广告策略研发(上海)
https://job.toutiao.com/s/sBDMAK

其他地区、其他职能线
https://job.toutiao.com/s/sB9Jqk


# 参考文章

[1] “西厢计划”原理小解
https://blog.youxu.info/2010/03/14/west-chamber/

[2] 从Linux协议栈代码和RFC看西厢计划原理
https://blog.csdn.net/dog250/article/details/7246895

[3] RFC 793 - TRANSMISSION CONTROL PROTOCOL
https://tools.ietf.org/html/rfc793
Apr 6

TCP:学得越多越不懂 不指定

felix021 @ 2020-4-6 13:25 [IT » 网络] 评论(0) , 引用(0) , 阅读(2070) | Via 本站原创
周末小课堂又开张了,这次我们来聊一聊TCP协议。


== 握手 ==

多少有点令人意外的是,大多数程序员对TCP协议的印象仅限于在创建连接时的三次握手。

严格地说,“三次握手”其实是一个不太准确的翻译,英文原文是 "3-way handshake",意思是握手有三个步骤。

不过既然教科书都这么翻译,我就只能先忍了。

“三次握手”的步骤相信各位都非常熟悉了:

引用
A: 喂,听得到吗 (SYN)
B: 阔以,你呢 (SYN-ACK)
A: 我也阔以,开始唠吧 (ACK)


(咦,这不是远程面试的开场白吗)

那么问题来了:为什么不是2次握手或者4次握手呢?


== 3次 ==

针对“为什么不是4次”,知乎的段子手是这么回答的:

引用
A: 喂,听得到吗 (SYN)
B: 阔以,你呢 (SYN-ACK)
A: 我也阔以,你呢 (SYN-ACK)
B: ...我不想和傻*说话 (FIN)


由此可见知乎质量的下降。


实际上,上面省略了真正重要的信息,在握手过程中传输的,不是“你能不能听得到”,而是:


引用
A: 喂,我的数据从x开始编号 (SYN)
B: 知道了,我的从y开始编号 (SYN-ACK)
A: 行,咱俩开始唠吧 (ACK)


协商一个序号的过程需要一个来回(告知 + 确认),理论上需要2个来回(4次),互相确认了双方的初始序号(ISN,Initial Sequence Number),才能真正开始通信。

由于第二个来回的“告知”可以和前一次的“确认”合并在同一个报文里(具体怎么结合后面讲),因此最终只需要3次握手,就可以建立起一个tcp链接。

这也解释了为什么不能只有2次握手:因为只能协商一个序号。

不过话说回来,知乎段子手的回复也不是全在抖机灵:毕竟,发起方怎么才能确认接收方已经知道发起方知道接收方知道了呢?即使发起方再问一遍,接收方又怎么知道发起方知道了接收方知道了呢?

很遗憾,结论是:无论多少个来回都不能保证双方达成一致。

由于实践中丢包率通常不高,因此最合理的做法就是3次握手(2个来回),少了不够,多了白搭;同时配上相应的容错机制。

例如 SYN+ACK 包丢失,那么发起方在等待超时后重传SYN包即可。

引用
想想看,如果最后一个ACK丢了会怎样?


然后问题又来了:为什么需要协商初始序号,才能开始通信呢?

== 可靠 ==

我们都知道,tcp是一个“可靠”(Reliable)的协议。

这里“可靠”指的不是保证送达,毕竟网络链路中存在太多不可靠因素。

在 IETF 的 RFC 793(TCP协议)中,Reliability的具体定义是:TCP协议必须能够应对网络通信系统中损坏、丢失、重复或者乱序发送的数据。

引用
Reliability:

The TCP must recover from data that is damaged, lost, duplicated, or delivered out of order by the internet communication system.

https://tools.ietf.org/html/rfc793


为了保证这一点,tcp需要给每一个 [字节] 编号:双方通过三次握手,互相确定了对方的初始序号,后续 [每个包的序号 - 初始序号] 就能标识该包在字节流中所处的位置,这样就可以通过重传来保证数据的连续性。

举个例子:

* 发送方(ISN=4000)
    * 发出 4001、4002、4003、4004
    * (假设每个包只有1字节的数据)
* 接收方
    * 收到 4001、4002、4004
    * 4003因为某种原因没有抵达
    * 这时上层应用只能读到4001、4002中的信息

由于接收方没有收到4003,因此给发送方的ACK中,序号最大值是4003(表示收到了4003之前的数据)。

过了一段时间(Linux下默认是1s),发送方发现4003一直没被ACK,就会重传这个包。

当接收方最终收到 4003 以后,上层应用才可以读到4003和4004,从而保证其收到的消息都是可靠的。(以及,接收方需要给发送方ACK,序号是4005)

注意:虽然ISN=4000,但是发送方发送的第一个包,SEQ是4001开始的,TCP协议规定 SYN 需要占一个序号(虽然SYN并不是实际传输的数据),所以前面示意图中ACK的seq是 x+1 。同样,FIN也会占用一个序号,这样可以保证FIN报文的重传和确认不会有歧义。

但是,为什么序号不能从 0 开始呢?

== 可靠² ==

真实世界的复杂性总是让人头秃。

我们知道,操作系统使用五元组(协议=tcp,源IP,源端口,目的IP,目的端口)来标识一个连接,当一个包抵达时,会根据这个包的信息,将它分发到对应的连接去处理。

一般情况下,服务器的端口号通常是固定的(如http 80),而操作系统会为客户端随机分配一个最近没有被使用的端口号,因此包总能被分发到正确的连接里。

但在某些特殊的场景下(例如快速、连续地开启和关闭连接),客户端使用的端口号也可能和上一次一样(或者用了其他刚断开的连接的端口号)。

而TCP协议并不对此作出限制:

引用
The protocol places no restriction on a particular connection being used over and over again. ... New instances of a connection will be referred to as incarnations of the connection.


那么:

* 如果前一个连接的包,因为某种原因滞留在网络中,这会儿才送达,客户端可能无法区分(其sequence number在本连接中可能是有效的)。

* 恶意第三方伪造报文的难度很小。注意,在这个场景里,第三方并 [不需要] 处于通信双方的链路之间,只要他发出的报文可以抵达通信的一方即可。

因此我们需要精心挑选一个ISN,使得上述case发生的可能性尽可能低。

注意:不是在tcp协议的层面上100%避免,因为这会导致协议变得更复杂,实现上增加额外的开销,而在绝大多数情况下是不必要的。如果需要“100%可靠”,需要在应用层协议上增加额外的校验机制;或者使用类似IPSec这样的网络层协议来保证对包的有效识别。

那么,ISN应该如何挑选呢?

== ISN生成器 ==

说起来其实很简单:

TCP协议的要求是,实现一个大约每 4 微秒加 1 的 32bit 计数器(时钟),在每次创建一个新连接时,使用这个计数器的值作为ISN。

假设传输速度是 2 Mb/s,连接使用的sequence number大约需要 4.55 小时才会溢出并绕回(wrap-around)到ISN。即使提高到 100 Mb/s,也需要大约 5.4 分钟。

而一个包在网络中滞留的时间通常是有限的,这个时间我们称之为MSL(Maximum Segment Lifetime),工程实践中一般认为不会超过2分钟。

所以我们一般不用担心本次连接的早期segment(tcp协议称之为 old duplicates)导致的混淆。

注:在家用千兆以太网已经逐渐普及、服务器间开始使用万兆以太网卡的今天,wrap-around的时间已经降低到32.8s(千兆)、3.28s(万兆),这个假定已经不太站得住脚了,因此 rfc1185 针对这种高带宽环境提出了一种扩展方案,通过在报文中加上时间戳,从而可以识别出这些 old duplicates。

主要风险在于前面提到的场景:前一个连接可能传输了较多数据,因此其序列号可能大于当前连接的ISN;如果该连接的报文因为某种原因滞留、现在又突然冒出来,当前连接将无法分辨。

因此,TCP协议要求在断开连接时,TIME-WAIT 状态需要保留 2 MSL 的时间才能转成 CLOSED(如下图底部所示)。

点击在新窗口中浏览此图片

(tcp连接状态图,截取自rfc 793)

那么问题又来了:为什么只有 TIME-WAIT 需要等待 2MSL,而LAST-ACK不需要呢?

== 报文 ==

针对TCP协议可以提的问题太多了,写得有点累,所以这里不打算继续自问自答了。

但写了这么多,还没有看一下TCP报文是什么结构的,实在不应该,这里还是祭出 rfc 793 里的 ascii art(并顺便佩服rfc大佬的画图功力)

点击在新窗口中浏览此图片

简单介绍下:

* 一行是4个字节(32 bits),header一般共5行(options和padding是可选的)
* 第一行包含了源端口和目的端口
    * 每个端口16bits,所以端口最大是65535
    * 源IP和目的IP在IP报文头里
* 第二行是本次报文的Sequence Number
* 第三行是ACK序列号
* 第四行包含了较多信息:
    * 数据偏移量:4字节的倍数,最小是0101(5),表示数据从第20个字节开始(大部分情况)
    * 控制位(CTL):一共6个,其中的ACK、SYN、FIN就不介绍了
    * RST是Reset,遇到异常情况时通知对方重置连接(我们敬爱的防火墙很爱用它)
    * URG表示这个报文很重要,应该优先传送、接收方应该及时给上层应用。URG的数据不影响seq,实际很少被用到,感兴趣的话可以参考下RFC 854(Telnet协议)
    * PSH表示这个报文不应该被缓存、应当立即被发送出去。在交互式应用中比较常用,如ssh,用户每按下一个键都应该及时发出去。注意和Nagle算法可能会有一些冲突。
    * 窗口大小:表示这个包的发送方当前可以接受的数据量(字节数),从这个包里的ack序号开始算起。**用于控制滑动窗口大小的关键字段就是它了。**

举个例子,三次握手的第二步,SYN和ACK合并的报文就是这么生成的:

* Sequence Number填入从ISN生成器中获取的值
* Acknowledgement Number填入 [发送方的序号 + 1]
* 将控制位中的ACK位、SYN位都置1

写不动了,真是没完没了(相信看到这里的同学已经不多了),但是TCP协议中还有很多有意思的设计本文完全没有涉及,文末我给出一些推荐阅读的链接,供感兴趣的同学参考。

== 总结 ==

* TCP“三次握手”翻译不准确
* 握手的目的是双方协商初始序列号ISN
* 序列号是用于保证通信的可靠性
* 不使用 0 作为ISN可以避免一些坑
* TCP报文里包含了端口号、2个序列号、一些控制位、滑动窗口大小
* 我在字节跳动网盟广告业务线(穿山甲),由于业务持续高速发展,长期缺人。关于字节跳动面试的详情,可参考我之前写的
    * 《程序员面试指北:面试官视角》
    * https://mp.weixin.qq.com/s/Byvu-w7kyby-L7FBCE24Uw

~ 投递链接 ~

后端开发(上海) https://job.toutiao.com/s/sBAvKe

后端开发(北京) https://job.toutiao.com/s/sBMyxk

广告策略研发(上海) https://job.toutiao.com/s/sBDMAK

其他地区、职能线 https://job.toutiao.com/s/sB9Jqk


== 推荐阅读 ==

[1] RFC 793:TRANSMISSION CONTROL PROTOCOL

https://tools.ietf.org/html/rfc793


[2] Coolshell - TCP 的那些事儿 (上 & 下)

https://coolshell.cn/articles/11564.html

https://coolshell.cn/articles/11609.html


[3] 知乎 - TCP 为什么是三次握手,而不是两次或四?

https://www.zhihu.com/question/24853633
Feb 25
0. 你得有一个 dnspod 帐号,并且把你的域名(例如 test.com )解析迁移过去(略)

1. 添加一个子域名的 A 记录,例如 ddns.test.com 指向 127.0.0.1

  $ export domain=test.com
  $ export subdomain=ddns

2. 生成一个token:参考官方说明 https://support.dnspod.cn/Kb/showarticle/tsid/227/

【务必注意】需要用生成的 ID 和 Token 这两个字段来组合成一个完整的 Token,组合方式为:"ID,Token"(用英文半角逗号分割),比如官方示例中,完整的 Token 为:13490,6b5976c68aba5b14a0558b77c17c3932 。

  $ export token=13490,6b5976c68aba5b14a0558b77c17c3932

3. 获取必要信息: 域名和子域名的ID

  $ curl -X POST https://dnsapi.cn/Record.List -d "login_token=${token}&format=json&domain=${domain}&sub_domain=${subdomain}"

返回结果为:{"status":{...}, "domain":{"id":640001, "name":"test.com", ...}, "info":{...}, "records":[{"id":"355300007", "name":"ddns", ...}]}

记录下对应域名的id 和子域名的id

  $ export domain_id=640001
  $ export subdomain_id=355300007

4. 获取外网ip

  $ wanip=`nc ns1.dnspod.net 6666`

5. 更新记录

  $ curl https://dnsapi.cn/Record.Ddns -d "login_token=${token}&format=json&domain_id=$domain_id&record_id=$record_id&sub_domain=$sub_domain&record_line=默认&value=$wanip"

= 完 =

(其实没完)其中 1、2、3 做完以后

6. 把 4、5 可以写到一个脚本里

  $ vi dnspod.sh
#!/bin/bash

domain_id=640001
record_id=355300007
sub_domain=ddns

wanip=`nc ns1.dnspod.net 6666`
curl https://dnsapi.cn/Record.Ddns -d "login_token=${token}&format=json&domain_id=$domain_id&record_id=$record_id&sub_domain=$sub_domain&record_line=默认&value=$wanip"


7. 设置 crontab

  $ crontab -e

引用
*/15 * * * * sh /path/to/dnspod.sh


=完=
Jan 27
AWS的http/https负载均衡挺好用的,但是有一点比较麻烦,因为是应用层协议,所以在后端的nginx(以及下面挂的php-fpm)看到的 REMOTE_ADDR是负载均衡的IP,而直连LB的IP,则是保存在了 HTTP_X_FORWARD_FOR 这个header里面。

当然,在应用里面增加一小段代码去解析这个header也不是什么难事,但是毕竟有些框架已经有一套解析的方案了,而且碰到客户端自己还用代理的时候,这个字段的value是一串IP列表(直连负载均衡的ip是最后一个),就变得更复杂了。比如:

$ curl https://test.com/ -H "X-FORWARDED-FOR: 8.8.8.8"

php记录下来的 $_SERVER 变量就长这样了:

array (
  'HTTP_X_FORWARDED_PORT' => '443',
  'HTTP_X_FORWARDED_PROTO' => 'https',
  'HTTP_X_FORWARDED_FOR' => '8.8.8.8, 13.31.11.23',
  'SERVER_PORT' => '80',  'SERVER_ADDR' => '172.24.22.33',
  'REMOTE_PORT' => '56247',
  'REMOTE_ADDR' => '172.24.22.34',
)

还是有点头疼的。幸好nginx有提供一个 ngx_http_realip_module 模块,可以解决这个问题,只要在配置里加上这么两行:

set_real_ip_from 172.24.0.0/16; #注:这个ip端是负载均衡所处VPC的CIDR
real_ip_header X-Forwarded-For;

重启nginx以后,再次访问就可以看到,REMOTE_ADDR 变成了 HTTP_X_FORWARDED_FOR 的 IP列表里最后一个IP。
Nov 23

记Yii2的一个坑 不指定

felix021 @ 2016-11-23 00:33 [IT » 网络] 评论(1) , 引用(0) , 阅读(12848) | Via 本站原创
最近发现有些服务器会定期出现磁盘过载问题,这里记录一下追查过程,供参考。

11月10日,立山向我反映我们线上的某 service 出现了一小段时间的无响应,查看 error log,发现有几百条"208203204 connect() to unix:/var/run/php5-fpm.sock failed (11: Resource temporarily unavailable) while connecting to upstream"错误,期间 zabbix 报警 server disk io overloaded,这让我想起确实每隔 3 ~ 4 天 zabbix 都会上报 server disk io overloaded(但出现的时间点并不固定在早上或晚上,也不一定是钱牛牛的访问高峰期),与 service 的error log时间也吻合,由于该 server 也是我们钱牛牛的两台 web 服务之一,因此在磁盘过载期间,钱牛牛对外提供的服务也收到了一定影响(error log也能证实这一点)。

用于 zabbix 的监控报警和 error log 的信息都太少,无法判断发生原因,因此没有继续追查下去;但是13日早晨这个问题又出现,因此决定重视起来。我在 server 上安装了 iotop 这个工具,使用 crontab 每分钟执行:
引用
$ /usr/sbin/iotop -btoqqqk --iter=5

每隔 1s 记录一次当前访问磁盘的进程及访问速度等信息,记录 5 次后退出。

在17号捕捉到又一次磁盘过载,通过 iotop 的输出:
引用
19:09:05  9663 be/4 nginx    31583.10 K/s 31551.68 K/s  0.00 % 93.86 % php-fpm: pool www

可以看到除了知道是 php-fpm 进程在写磁盘之外,并没有什么卵用,但至少还是指明了方向,只要找出 php 在写什么文件,就能离发现原因更近。

因此我写了另一个 monitor.py (后附),实时监控 iotop 的输出,筛选出磁盘 io 过大的进程,找出这些进程打开的文件(ls -lh /proc/$PID/fd),上报到sentry:
引用
$ /usr/sbin/iotop -btoqqqk | ./monitor.py


又等了5天,今天(22号)终于抓到罪魁祸首:

    server: PID(4252) IS USING TOO MUCH DISK IO
    {
        "iotop": "07:44:17  4252 be/4 nginx    288.76 K/s 94813.59 K/s  0.00 % 75.15 % php-fpm: pool www",
        "proc": "/proc/4252/fd:
    total 0
    lrwx------ 1 nginx users 64 Nov 22 07:44 0 -> socket:[1286391831]
    lrwx------ 1 nginx users 64 Nov 22 07:44 1 -> /dev/null
    lrwx------ 1 nginx users 64 Nov 22 07:44 2 -> /dev/null
    lrwx------ 1 nginx users 64 Nov 22 07:44 3 -> socket:[2138228391]
    lrwx------ 1 nginx users 64 Nov 22 07:44 4 -> socket:[2138229146]
    l-wx------ 1 nginx users 64 Nov 22 07:44 5 -> /data/www/xxx-service/runtime/logs/app.log
    lr-x------ 1 nginx users 64 Nov 22 07:44 6 -> /data/www/xxx-service/runtime/logs/app.log
    ",
        "time": "2016-11-21 07:44:17"
    }

从这里可以看出,php-fpm是在读写 service 的log。log文件内容有点琐碎,但是跟往常比起来确实没有什么异常,但是文件本身有点异常:

引用
nginx@server:logs$ ls -lah
total 5.0G
drwxrwxrwx 2 nginx users  4.0K Nov 22 10:54 .
drwxrwxrwx 3 nginx users  4.0K Jul 28 15:15 ..
-rwxrwxrwx 1 nginx users  55M Nov 22 12:04 app.log
-rw-r--r-- 1 nginx users 1001M Nov 22 07:44 app.log.1
-rw-r--r-- 1 nginx users 1001M Nov 22 07:43 app.log.2
-rw-r--r-- 1 nginx users 1001M Nov 22 07:42 app.log.3
-rw-r--r-- 1 nginx users 1001M Nov 22 07:41 app.log.4
-rw-r--r-- 1 nginx users 1001M Nov 22 07:40 app.log.5


可以看出,所有的log文件都是在磁盘负载特别高的时候修改的,可见,磁盘负载高的直接原因是 yii 框架的 logrotate 机制导致的。

以下是从 yii2/framework2/vendor/yiisoft/yii2/log/FileTarget.php 拷贝出来的内容:
public $rotateByCopy = true;
...
protected function rotateFiles()
{
    $file = $this->logFile;
    for ($i = $this->maxLogFiles; $i >= 0; --$i) {
        // $i == 0 is the original log file
        $rotateFile = $file . ($i === 0 ? '' : '.' . $i);
        if (is_file($rotateFile)) {
            // suppress errors because it's possible multiple processes enter into this section
            if ($i === $this->maxLogFiles) {
                @unlink($rotateFile);
            } else {
                if ($this->rotateByCopy) {
                    @copy($rotateFile, $file . '.' . ($i + 1));
                    if ($fp = @fopen($rotateFile, 'a')) {
                        @ftruncate($fp, 0);
                        @fclose($fp);
                    }
                } else {
                    @rename($rotateFile, $file . '.' . ($i + 1));
                }
            }
        }
    }
}


可以看出,罪魁祸首是 $rotateByCopy 默认值是 true ,而 yii2 之所以这么做,(根据框架的注释)是因为在 windows 下 log文件 很可能正被另一个文件打开,导致 rename 失败(吐槽:难道就不能多写一行代码根据检测到的os的type设置这个值吗???)。这也解释了为什么每个被 rotate 的 log 文件的修改时间间隔1分钟。

既然找到了问题的原因,解决方案就很简单了,把这个属性修改为false即可,当然,更完善的方案是能够根据OS的类型自动检测这个值。根据这个思路,我向 yii2 官方提交了一个pull requests:https://github.com/yiisoft/yii2/pull/13057,希望能被 merge 进去吧。

完。


monitor.py:

#!/usr/bin/python
#coding:utf-8

import sys
import re
import time
import datetime
import socket
try:
    import simplejson as json
except:
    import json

import subprocess
from raven import Client

import requests

last_sent = 0

dsn = '__SENTRY_DSN__'

#00 - '19:07:03'
#01 - '9663'
#02 - 'be/4'
#03 - 'nginx'
#04 - '10423.06'
#05 - 'K/s'
#06 - '10423.06'
#07 - 'K/s'
#08 - '0.00'
#09 - '%'
#10 - '99.99'
#11 - '%'
#12 - 'php-fpm: pool www'

def should_skip(program):
    if program == '[kjournald]':
        return True

    for prefix in ['gzip', 'rsync', 'ssh', 'logrotate', 'sap100', 'sar ', 'rpm ', 'updatedb', 'mysql', 'nginx', 'vim', 'cat']:
        if program.startswith(prefix):
            return True

    return False

def run_command(*cmd):
    try:
        p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
        out, err = p.communicate()
        if err:
            raise Exception(err.strip())
        return out
    except Exception, e:
        return 'run %s failed: %s' % (str(cmd), e)

while True:
    try:
        line = sys.stdin.readline().strip()
    except KeyboardInterrupt:
        print >>sys.stderr, "user abort"
        sys.exit(0)

    fields = re.split(' +', line.strip(), 12)
    if len(fields) != 13:
        continue

    if should_skip(fields[12]):
        continue

    read_speed  = float(fields[4])
    write_speed = float(fields[6])
    if read_speed > 1000 or write_speed > 1000:
        date = time.strftime('%Y-%m-%d')
        pid = fields[1]
        client = Client(dsn)
        message = '%s: PID(%s) IS USING TOO MUCH DISK IO' % (socket.gethostname(), pid)
        args = {
            'time'  : date + ' ' + fields[0],
            'iotop' : line.strip(),
            'proc'  : run_command('ls', '-lhR', '/proc/%s/fd' % pid),
        }
        print >>sys.stderr, message
        print >>sys.stderr, json.dumps(args, indent=4)
        client.capture('raven.events.Message', message=message, extra=args)
Mar 11

Hi, HTTPS 不指定

felix021 @ 2016-3-11 01:16 [IT » 网络] 评论(0) , 引用(0) , 阅读(10077) | Via 本站原创
Let's Encrypt项目进入Public Beta已经好久拉,不过因为使用dnspod作为域名的解析服务提供商,该项目官方刚开始的时候并不被支持(总是报错),所以拖了好久,今天才终于搞起来。

用起来还真是超简单:

1. 获取证书

$ git clone https://github.com/letsencrypt/letsencrypt
$ cd letsencrypt
$ ./letsencrypt-auto certonly -w /path/to/www-root felix021.com www.felix021.com

没有什么意外的话,获取到的证书就存在 /etc/letsencrypt/live/felix021.com/ 下面

2. 配置nginx

最简单的就是加上三行:
引用
    listen 443 ssl;
    ssl_certificate /etc/letsencrypt/live/felix021.com/cert.pem;
    ssl_certificate_key /etc/letsencrypt/live/felix021.com/privkey.pem;


但是比较推荐的做法是增加一个80端口redirect到https的配置:
引用
server {
    listen 80;
    server_name felix021.com www.felix021.com;
    return 301 https://www.felix021.com$request_uri;
}


然后执行 service nginx reload ,就生效了。

p.s. 对于我使用的bo-blog博客系统,还有一个坑,就是需要在blog设置里面将URL路径的http换成https,否则使用相对路径引用的css等静态文件资源还是会引用到http去(这是多么奇葩的一个特性啊...)

3. 定期更新

该项目提供的整数有效期只有90天,似乎短了点,但是实际上因为提供了命令行自动更新的方式,并不会造成多大困然,反而可以缩短因为证书泄漏而导致的风险期(貌似也提供了revoke功能,不过我没尝试)。因此官方宣称证书的有效期未来可能会进一步缩短。

想要renew证书,最直接的方式就是前面的certonly命令带上完整参数再跑一次。官方还提供了一个更简单的"letsencrypt renew"命令,会读取上次的配置来重新获取证书。

基于此可以写一个简单的renew脚本,放到root的crontab里,每个月跑一次就好拉。记得renew完要service nginx reload就好啦。
分页: 1/26 第一页 1 2 3 4 5 6 7 8 9 10 下页 最后页 [ 显示模式: 摘要 | 列表 ]