eBPF挂钩TC egress时,被TCP Checksum搞死了 译者注 我最近也在尝试用eBPF TC类型程序,挂钩egress网络包,修改IP包、TCP包内容,实现路由跟踪的功能。除了eBPF验证器的奇葩问题外,剩下的就是skb_buff修改后,被客户端内核丢弃、中间路由丢弃、服务端内核丢弃的各种问题,头发那是一把一把的掉。幸运的是,我找到了这篇文章Checksum-or-fxxk-up,写的特别好。看标题就知道作者被折腾不清,这点我感同身受。 TL;DR 如果你不想自讨苦吃,就千万别对checksum进行任何不当操作。但如果你想看到一个反面教材,继续阅读这篇由我这个不称职的人写的记录。 背景 我有一个挂钩在 ingress TC BPF程序,可以修改某些传入数据包的IP头部和TCP标志中的几个位。它在功能上表现得非常出色。直到... dmesg 异常日志 在测试程序时,在dmesg中发现了以下消息: [1216218.835883] <unknown>: hw csum failure [1216218.843767] CPU: 11 PID: 3627719 Comm: ThriftIO15 Not tainted 4.16.18-210_fbk23_5358_gf2838c074351 #210 [1216218.862917] Hardware name: Quanta Twin Lakes MP/Twin Lakes Passive MP, BIOS F09_3A12 10/08/2018 [1216218.880669] Call Trace: [1216218.885910] <IRQ> [1216218.890287] dump_stack+0x46/0x68 [1216218.897272] __skb_checksum_complete+0xb0/0xc0 [1216218.906516] tcp_v6_do_rcv+0x12b/0x3d0 [1216218.914370] tcp_v6_rcv+0xad0/0xb00 [1216218.921701] ip6_input_finish+0xbf/0x460 [1216218.929903] ip6_input+0x80/0x90 [1216218.936714] ipv6_rcv+0x358/0x4c0 [1216218.943698] ? tcf_classify+0x87/0x120 [1216218.951551] __netif_receive_skb_core+0x541/0xa40 [1216218.961315] netif_receive_skb_internal+0x24/0xb0 [1216218.971080] napi_gro_receive+0xba/0xe0 [1216218.979105] mlx5e_handle_rx_cqe+0x83/0xd0 [1216218.987649] mlx5e_poll_rx_cq+0xc8/0x920 [1216218.995840] mlx5e_napi_poll+0xa1/0xc30 [1216219.003866] net_rx_action+0x128/0x340 [1216219.011718] __do_softirq+0xd4/0x2ad [1216219.019219] irq_exit+0xa5/0xb0 [1216219.025850] do_IRQ+0x7d/0xc0 [1216219.032132] common_interrupt+0xf/0xf [1216219.039798] </IRQ> 我是如何陷入checksum深渊的。 根因与解决方案 这条消息显示出现了checksum错误。我让tcpdump重新检查,但他说checksum是正确的。然后我意识到入口TC BPF程序在tcpdump之后、内核接收数据包之前运行。这可以解释为什么tcpdump在checksum方面没有问题,而内核却报错。 根本原因很明确:入口BPF程序在TCP/IP头部篡改了位,但没有修复checksum。解决方案似乎也很简单:修复checksum就好了。 嗯,我将为我们节省一些时间,不再抱怨在一大堆与checksum相关的API中查找解决方案时的痛苦,比如bpf_l3_csum_replace、bpf_l4_csum_replace、bpf_csum_diff和bpf_csum_update。 更令人沮丧的是,最后一个简单的BPF_F_RECOMPUTE_CSUM标志传递给bpf_skb_store_bytes(用于修改位)就解决了问题。 checksum工作原理 在深入了解更多细节之前,让我们来看看checksum的工作原理。下面的图示非常好地阐述了这一点。(图片版权:Error Detection in Computer Networks) 为了避免混淆,在这篇文章中,我将使用术语“csum”来指代上图中的“sum”,并使用术语“checksum”来指代“csum”的1's补码。正如你所看到的,只要数据没有被篡改,~(csum + checksum) == 0。 还有一个谜题 在调试过程中,我发现了一件有趣的事情 - 尽管错误被打印出来,但数据包并没有被丢弃,因此处理过程继续进行。为什么会这样呢? 由于上面的堆栈跟踪中没有显示某些内联函数,所以我将在这里“展开”调用堆栈的逻辑: tcp_v6_do_rcv() { // Checking checksum is unnecessary if (skb_csum_unnecessary(skb)) { nothing; } else { // Re-do checksum for whole skb csum = skb_checksum(skb, 0, skb->len, 0); // Check csum against skb->csum. If skb is intact, sum should be 0 sum = csum_fold(csum_add(skb->csum, csum)); if (sum == 0) Print "hw csum failure" to dmesg and dump stack trace else Checksum fails, throw the skb away } // Continue processing skb } 当打印错误时,必须满足以下条件: skb_csum_unnecessary返回false。 如果我们计算skb的checksum并将其与skb->csum进行比较,应该通过。也就是说,csum_add(skb->csum, skb_checksum(skb))应该返回零。 当skb_csum_unnecessary返回false时,意味着skb->csum_valid为false,这反过来又意味着在skb_checksum_init期间未将skb->csum_valid设置为1。这是因为原始的skb->csum和psum不相加得到零。还要注意,在skb_checksum_init之后,将skb->csum设置为psum。 说人话就是: 如果我正在运行,则表示检查硬件(NIC)提供的 skb-> csum 与 psm (稍后会讨论) 的一致性检查失败了。去你妈的硬件!(打印错误)但你知道吗?我重新计算了 skb 的checksum,并且它确实正确无误。让我们忘记硬件错误继续前进吧! 毫无疑问,硬件是清白的,罪魁祸首是我的 BPF 程序。所以即使数据包没有被丢弃,并且连接没有受到影响,我仍然修复了有罪的 BPF 程序来证明硬件的清白。 当我庆祝又一个结案时,我不知道罪犯正在干什么。 checksum又TM搞事情? 在部署BPF程序时,我发现一旦程序被attach,InCsumErrors就会不断增加。 $ netstat -s | grep InCsumErrors InCsumErrors: 101 继续调试 我不能谈论内核调试而不提到我的好朋友 - bpftrace。这一次,再次借助它的帮助,我能够将问题缩小到以下堆栈: __skb_checksum_complete+1 tcp_v6_do_rcv+293 tcp_v6_rcv+2762 ip6_input_finish+186 ip6_input+128 ipv6_rcv+853 __netif_receive_skb_core+1312 netif_receive_skb_internal+36 napi_gro_receive+183 mlx5e_handle_rx_cqe+125 mlx5e_poll_rx_cq+194 mlx5e_napi_poll+161 net_rx_action+290 __softirqentry_text_start+207 irq_exit+165 do_IRQ+119 ret_from_intr+0 cpuidle_enter_state+176 do_idle+260 cpu_startup_entry+25 secondary_startup_64+165 堆栈与之前的问题相同。然而,这次在dmesg中没有错误消息,并且数据包被丢弃,导致了重传。在tcpdump中,我看到的是以下序列: 一个TCP SYN数据包到达,但没有收到SYN-ACK。 一秒钟后,重新发送的SYN数据包到达并收到了SYN-ACK。 连接成功建立。 老天保佑 在浏览内核代码数小时后,我决定采用一种不太科学但有效的调试方法 - 尝试不同的事物,希望能找到一个可行的解决方案,并祈求上天提高成功率。 然后我注意到问题只发生在修改TCP头部时。即使使用BPF_F_RECOMPUTE_CSUM在修改后相应地修复了skb->csum。我决定看一下如果在修改TCP头部后不修复skb->csum会发生什么。而且,令人惊讶的是,它起作用了! 以下表格显示了我尝试过的事情及其结果: 至少有两个问题出现在这个表格中: 如果不重新计算IP头部更改的checksum,为什么会出现hw csum failure? 如果重新计算TCP头部更改的checksum,为什么会出现TcpInCsumErrors? 下一个目标是回答这两个问题,并确保我发现了解决方案而不是巧合。 寻找答案 TcpInCsumErrors错误发生是因为skb_checksum_init返回非零值。调用堆栈有点复杂。所以让我们再次简化逻辑(省略了无关的代码)。 这是我见过的最复杂的代码之一。 我在上述代码中标记了两个错误情况。我们需要非常小心地处理逻辑,以避免在处理checksum时出现这两个错误。 ip6_compute_pseudo 是什么? 它计算 TCP 伪头部的checksum。结果就是我们之前看到的 psum。(图片来源:[TCP Checksum Calculation and the TCP "Pseudo Header"](http://www.tcpipguide.com/free/t_TCPChecksumCalculationandtheTCPPseudoHeader-2.htm "TCP Checksum Calculation and the TCP "Pseudo Header"")) 触发“hw csum failure”的条件如下: ~(psum + skb->csum) != 0,即检查skb->csum与psum的校验失败。 ~(skb_checksum(skb, 0, skb->len, 0) + psum) == 0,即对skb新计算的checksum与psum进行校验通过。 触发“TcpInCsumErrors”的条件如下: ~(psum + skb->csum) != 0,即检查skb的checksum与psum的校验失败。 ~((skb_checksum(skb, 0, skb->len, 0) + psum) != 0,即对skb新计算的checksum与psum进行校验也失败。 有些奇怪 你可能注意到了一些奇怪的地方。skb->csum与psum(伪头部的checksum)进行了比较。为什么会这样呢?难道不应该使用类似于~(skb->csum + checksum(skb)) == 0这样的方式,将其与整个skb的checksum进行比较吗? 我会尽力解释一下。关键在于,就像上面显示的图表所示,TCP头部也有一个checksum。 计算TCP头部checksum的公式是 tcp_hdr_checksum = ~(pseudo_hdr + tcp_hdr_with_checksum_0) 注意: 我将使用“+”来表示“csum add”,这个算法将展示“checksum是如何工作的?”部分的图表中。 payload被故意省略,因为它并不重要。 skb->csum 是由NIC使用整个数据包计算得出的16位checksum。即, skb->csum = ip_hdr + tcp_hdr 注意: 在进行csum add时,IP头和TCP头都会被切片,然后将它们的16位块相加/折叠在一起。 因为IP头、伪头部和TCP头都是16位对齐的,所以这两个公式可以重新写成 tcp_hdr_checksum = ~(pseudo_hdr_0_15 + pseudo_hdr_16_31 + ... + tcp_hdr_0_15 + tcp_hdr_16_31 + ... + 0x0000 (checksum) + ...) 如果我们在两边都进行~操作,我们得到 ~tcp_hdr_checksum = pseudo_hdr_0_15 + pseudo_hdr_16_31 + ... + tcp_hdr_0_15 + tcp_hdr_16_31 + ... + 0x0000 (checksum) + ... 然后我们在两端添加tcp_hdr_checksum: ~tcp_hdr_checksum + tcp_hdr_checksum = pseudo_hdr_0_15 + pseudo_hdr_16_31 + ... + tcp_hdr_0_15 + tcp_hdr_16_31 + ... + tcp_hdr_checksum + ... 因为我们知道 ~tcp_hdr_checksum + tcp_hdr_checksum = 0xFFFF。所以 pseudo_hdr_0_15 + pseudo_hdr_16_31 + ... + tcp_hdr_0_15 + tcp_hdr_16_31 + ... + tcp_hdr_checksum + ... = 0xFFFF 因此 tcp_hdr_0_15 + tcp_hdr_16_31 + ... + tcp_hdr_checksum + ... = 0xFFFF - (pseudo_hdr_0_15 + pseudo_hdr_16_31 + ... ) = 0xFFFF - psum 我们可以将这个插入到skb->csum中。 skb->csum = ip_hdr_0_15 + ip_hdr_16_31 + ... + tcp_hdr_0_15 + tcp_hdr_16_31 + ... + tcp_hdr_checksum + ... = ip_hdr_0_15 + ip_hdr_16_31 + ... + 0xFFFF - psum 记住这个公式,我会回答上面的两个问题。 为什么在不重新计算IP头更改的情况下会出现“hw csum failure”? (为简单起见,假设TCP头部未被修改。) 在内核网络堆栈中,在处理IP头部之后,它的checksum会从skb->csum中提取出来或者减去。然后skb->csum就变成了 skb->csum = (ip_hdr_0_15 + ip_hdr_16_31 + ... + 0xFFFF - psum) - (ip_hdr_0_15 + ip_hdr_16_31 + ...) = 0xFFFF - psum 现在我们知道为什么在skb_checksum_init函数中,要将skb->csum与psum进行比较 - 因为它是公式中唯一剩下的变量。 然而,如果IP头部发生了改变(比如说,ip_hdr_16_31中的几个位被翻转),但是skb->csum没有相应地改变,那么它就会变成这样。 skb->csum = skb->csum - (ip_hdr_0_15 + ip_hdr_16_31 + ...) = (ip_hdr_16_31 - ip_hdr_16_31) + 0xFFFF - psum 第一个条件 ~(psum + skb->csum) != 0或者(psum + skb->csum) != 0xFFFF。而数学计算如下: psum + skb->csum = psum + (ip_hdr_16_31 - ip_hdr_16_31) + 0xFFFF - psum = (ip_hdr_16_31 - ip_hdr_16_31) + 0xFFFF 结果显然不是0xFFFF。因此第一个条件已满足。 第二个条件 ~(skb_checksum(skb, 0, skb->len, 0) + psum) == 0或者skb_checksum(skb, 0, skb->len, 0) + psum == 0xFFFF。 skb_checksum(skb, 0, skb->len, 0) 是用于重新计算 skb 的checksum,此时 skb 只包含 TCP 头部。这将会 skb_checksum(skb, 0, skb->len, 0) = tcp_hdr_0_15 + tcp_hdr_16_31 + ... + tcp_hdr_checksum + ... = 0xFFFF - psum 因此 skb_checksum(skb, 0, skb->len, 0) + psum = 0xFFFF - psum + psum = 0xFFFF 第二个条件也得到满足。问题已经得到回答。 为什么在重新计算TCP头部checksum时会出现“TcpInCsumErrors”? (为简单起见,假设TCP头未被更改。) 请注意,当我们使用BPF_F_RECOMPUTE_CSUM时,它只会更新skb->csum而不会更新tcp_hdr_checksum。假设tcp_hdr_0_15中的几个位被翻转了。当调用skb_checksum_init时,实际上是在使用skb->csum。 skb->csum = tcp_hdr_0_15 + tcp_hdr_16_31 + ... + tcp_hdr_checksum + ... = tcp_hdr_0_15 - tcp_hdr_0_15 + tcp_hdr_0_15 + tcp_hdr_16_31 + ... + tcp_hdr_checksum + ... = tcp_hdr_0_15 - tcp_hdr_0_15 + 0xFFFF - psum 第一个条件 “TcpInCsumErrors”的判断条件为~(psum + skb->csum) != 0 或者 (psum + skb->csum) != 0xFFFF。所以数学计算如下: psum + skb->csum = psum + tcp_hdr_0_15 - tcp_hdr_0_15 + 0xFFFF - psum = tcp_hdr_0_15 - tcp_hdr_0_15 + 0xFFFF 结果显然不是0xFFFF。因此第一个条件已满足。 第二个条件 “TcpInCsumErrors”的~(skb_checksum(skb, 0, skb->len, 0) + psum) != 0或者skb_checksum(skb, 0, skb->len, 0) + psum != 0xFFFF。 skb_checksum(skb, 0, skb->len, 0) 是重新计算 skb 的checksum。它将与 skb->csum 相同,因此数学上与第一个条件相同。换句话说,第二个条件也满足。 正如你所看到的,如果我们在更新tcp_hdr_0_15时不修复skb->csum,那么第一个条件将无法满足,代码将直接从快乐结局路径返回。 等一下,这不是一个黑客行为吗? 是的,我同意。更新TCP头部但不更新其checksum和skb->csum听起来像个黑客行为。另一种选择是: 更新TCP头部中的位。 更新skb->csum以反映位变化。 更新TCP校验字段以反映位变化。 更新skb->csum以反映TCP checksum的变化。 通过这种方式,TCP头部的差异和TCP checksum的差异相互抵消。数学计算如下: skb->csum = tcp_hdr_0_15 + tcp_hdr_16_31 + ... + tcp_hdr_checksum + ... = tcp_hdr_0_15 - tcp_hdr_0_15 + tcp_hdr_0_15 + tcp_hdr_16_31 + ... + tcp_hdr_checksum - tcp_hdr_checksum + tcp_hdr_checksum + ... = (tcp_hdr_0_15 - tcp_hdr_0_15) + (tcp_hdr_checksum - tcp_hdr_checksum) + 0xFFFF - psum = 0xFFFF - psum 既然结果与根本不触碰skb->csum相同,为什么不直接使用这个技巧呢? 要点 如果可以的话,请不要搞乱checksum。 发明checksum的人是一个天才。 我是个傻X。 2023-07-24 0 0
在mac osx上看lol国服ob录像的技术分析 一天,兴起而至,想在mac osx上看lol ob,要看国服的,没现成方案,实现不了,未遂,便决定研究一番。 在拳头游戏官方的api中SPECTATING GAMES riot games api提到观看ob模式的程序启动参数: "C:\Riot Games\League of Legends\RADS\solutions\lol_game_client_sln\releases\0.0.1.74\ deploy\League of Legends.exe" "8394" "LoLLauncher.exe" "" "spectator spectator.na.lol.riotgames.com:80 P+C3YqI3Mg9oHc6t9eTAKWE4T8prxwzR 1726229459 NA1" 程序启动的第三个参数中: 这里面包括ob server地址、端口,以及本场录像文件加密key,以及游戏id,游戏所属平台id。其中 encryptionKey参数每次都不一样,从数据包分析发现,这个key并不是为了在获取录像时的验证,我觉得是roitgames为了安全,为每场游戏的协议数据包做的加密key,客户端观看ob 录像时,用这个key解密,以提高协议安全度。 在riotgames官方api介绍上,以及github上几个开源的lol项目上,以及本人wireshark抓包分析总结出一场ob游戏观看,一共调用了如下http api接口 GET /observer-mode/rest/consumer/version //获取录像文件版本信息,返回字符串 GET /observer-mode/rest/consumer/getGameMetaData/:region/:id/*ignore //获取游戏的基本信息,包括游戏当前桢,最后一个chunkid,游戏开始时间等信息。返回json格式字符串 GET /observer-mode/rest/consumer/getLastChunkInfo/:region/:id/:end/*ignore //获取录像中最后一个chunk基本信息,包括了当前chunkid,下一个chunkid,当前桢的id之类信息,返回json格式字符串 GET /observer-mode/rest/consumer/getGameDataChunk/:region/:id/:chunkId/*ignore //获取对应chunkid的协议数据,返回字节流 GET /observer-mode/rest/consumer/getKeyFrame/:region/:id/:frame/*ignore //获取对应chunkid的协议数据,返回字节流 lol ob观战模式的实现,是将游戏中所有玩家操作,合并成一个chunk块,来发送到客户端的,以减少通讯量。 GET /record/:region/:gameId/:encryptionKey 这个api在github上有提到过,但我抓包过程中,没有看到被调用过。暂时忽略。 至此已经知道lol 英雄联盟的观战通讯过程,但全球大区中,我们腾讯服务器跟欧美服务器不一样,并没有公开的ob server地址。而是在tgp腾讯游戏助手中实现的。 这是个嵌入的html,真实地址是 http://api.tgp.qq.com/spectator/v1410/watch.shtml ,对应的ob 观战列表文件为 http://api.pallas.tgp.qq.com/core/get_ob_list ,以ob文件http://ob.pallas.tgp.qq.com/ob_data/1_1632443320.ob 来举例子。对于国外其它服务器的观战,是连接到官方的观战服务器进行的。而腾讯lol的观战,却是将录像文件下载到本地的,那是否可说明tgp助手在本地建立了一个http服务器,并解析ob文件,实现ob观战服务器的功能呢? 为此开始确认这个过程,在tgp腾讯游戏助手上观战了这场游戏,查看游戏进程的启动参数中,ob server地址是多少,开Process Monitor抓进程启动参数,开tgp助手观战: 如上图所示,启动参数复合riotgame的api介绍,我们来确认下127.0.0.1: 36722 是否正确,以及是哪个进程开启的。 replay.exe进程启动详情: 腾讯tgp助手,确实在本地开启http ob server来实现的。那么进一步抓包验证。对于回环地址,wireshark抓不到的,要用RawCap来抓。 同时,wireshark 抓取 ob录像文件下载的数据包,查看localhost.pcanpng ,果然是lol ob录像系统调用的几个http请求。 一一查看version\getGameMetaData\getLastChunkInfo\getGameDataChunk\getKeyFrame几个请求。也确实以相应字符串、json字符串、字节流的形式返回,但数据源头来自哪里呢?肯定是最初的ob文件了。 如上图,其中开头的每行分隔的字符串,、json字符串也比较好理解,里面的元素也是lol ob录像系统请求相应的json字符串 LOLOB/1.0 abstract:{"src": 1, "area_id": 1, "score": 5625000, "game_length": 1260, "battle_type": 4, "max_tier": [0, 0], "game_id": 1632443320, "start_time": "2015-10-21 08:13:37", "ob_ver": "1.82.89", "encryption_key": "x83U9UPUuB/+INJyaU2Wz8lUuLn5aXEt"} source:{"gameId": 1632443320, "gameStartTime": 1445386417603, "platformId": "HN1", "gameMode": "CLASSIC", "mapId": 11, "gameType": "MATCHED_GAME", "gameQueueConfigId": 4, "observers": {"encryptionKey": "x83U9UPUuB/+INJyaU2Wz8lUuLn5aXEt"}, "participants": [{"profileIconId": 7, "championId": 60, "summonerName": "\u5fc3\u6001\u597d\u80fd\u4e0a\u5206\u5417", "skinIndex": 2, "bot": false, "spell2Id": 11, "teamId": 100, "spell1Id": 4}, {"profileIconId": 15, "championId": 117, "summonerName": "\u4e5d\u670813\u65e5\u4e36", "skinIndex": 5, "bot": false, "spell2Id": 4, "teamId": 100, "spell1Id": 12}, {"profileIconId": 841, "championId": 58, "summonerName": "\u501a\u7af9\u8f7b\u8bed\u767d\u886b\u5982\u6545", "skinIndex": 2, "bot": false, "spell2Id": 4, "teamId": 100, "spell1Id": 12}, {"profileIconId": 17, "championId": 429, "summonerName": "\u840c\u515c\u515cTcT", "skinIndex": 2, "bot": false, "spell2Id": 7, "teamId": 100, "spell1Id": 4}, {"profileIconId": 914, "championId": 122, "summonerName": "\u91cd\u5e86\u897f\u5357\u5927\u5b66\u6821\u8349", "skinIndex": 0, "bot": false, "spell2Id": 4, "teamId": 100, "spell1Id": 14}, {"profileIconId": 922, "championId": 105, "summonerName": "L4ugh1ng", "skinIndex": 8, "bot": false, "spell2Id": 12, "teamId": 200, "spell1Id": 4}, {"profileIconId": 1, "championId": 28, "summonerName": "\u5170\u4ead\u5e8f", "skinIndex": 0, "bot": false, "spell2Id": 4, "teamId": 200, "spell1Id": 11}, {"profileIconId": 9, "championId": 203, "summonerName": "\u8292\u5e02\u9189\u9999\u56ed\u996d\u5e84", "skinIndex": 0, "bot": false, "spell2Id": 4, "teamId": 200, "spell1Id": 12}, {"profileIconId": 931, "championId": 110, "summonerName": "15\u5c81\u5357\u660c\u4eba\u670d\u4e0d\u670d", "skinIndex": 0, "bot": false, "spell2Id": 4, "teamId": 200, "spell1Id": 7}, {"profileIconId": 28, "championId": 432, "summonerName": "\u6218\u4e36\u65d7TV\u767d\u8272\u98ce\u8f66", "skinIndex": 1, "bot": false, "spell2Id": 14, "teamId": 200, "spell1Id": 4}], "gameTypeConfigId": 2, "gameLength": 5, "bannedChampions": [{"teamId": 100, "championId": 41, "pickTurn": 1}, {"teamId": 200, "championId": 157, "pickTurn": 2}, {"teamId": 100, "championId": 82, "pickTurn": 3}, {"teamId": 200, "championId": 114, "pickTurn": 4}, {"teamId": 100, "championId": 4, "pickTurn": 5}, {"teamId": 200, "championId": 76, "pickTurn": 6}]} obmeta:{"lastChunkId": 14, "gameKey": {"platformId": "HN1", "gameId": 1632443320}, "startTime": "Oct 21, 2015 8:13:37 AM", "endGameKeyFrameId": -1, "gameLength": 0, "keyFrameTimeInterval": 60000, "port": 0, "gameServerAddress": "", "interestScore": 2989, "clientBackFetchingEnabled": false, "clientAddedLag": 30000, "endStartupChunkId": 7, "chunkTimeInterval": 30000, "clientBackFetchingFreq": 1000, "pendingAvailableKeyFrameInfo": [], "decodedEncryptionKey": "", "createTime": "Oct 21, 2015 8:10:24 AM", "featuredGame": true, "gameEnded": false, "encryptionKey": "", "delayTime": 150000, "endGameChunkId": -1, "pendingAvailableChunkInfo": [], "startGameChunkId": 9, "lastKeyFrameId": 3} keyframe_tab:[[1, 9], [2, 11], [3, 13], [4, 15], [5, 17], [6, 19], [7, 21], [8, 23], [9, 25], [10, 27], [11, 29], [12, 31], [13, 33], [14, 35], [15, 37], [16, 39], [17, 41], [18, 43], [19, 45], [20, 47], [21, 49]] chunk_tab:[[1, 0], [2, 0], [3, 0], [4, 0], [5, 0], [6, 0], [7, 0], [8, 0], [9, 30018], [10, 30008], [11, 29986], [12, 30007], [13, 30000], [14, 29985], [15, 30019], [16, 29981], [17, 30000], [18, 30008], [19, 30003], [20, 29982], [21, 30012], [22, 30011], [23, 29980], [24, 29993], [25, 30018], [26, 29982], [27, 30018], [28, 29984], [29, 30020], [30, 29984], [31, 30016], [32, 29980], [33, 29994], [34, 30009], [35, 30016], [36, 29997], [37, 29976], [38, 30006], [39, 30008], [40, 29985], [41, 30000], [42, 30027], [43, 29972], [44, 30006], [45, 30016], [46, 29977], [47, 30024], [48, 29990], [49, 29994], [50, 20015]] 不难看出,其中“LOLOB/1.0”为tgp助手本身识别的版本信息;abstract是ob录像版本,以及录像数据加密key存储的基本信息;obmeta对应着getGameMetaData的数据;keyframe_tab跟chunk_tab暂时不知道是什么意思,可稍后再回来看。 ob文件的剩下其它部分,显然就是录像中,每个chunk包的录像数据的了,那它是如何拼装的呢? 腾讯tgp助手下载的ob文件中,录像部分开头的字节流是01 00 01 00 00 35 88 85 a0 4a 30 1b 30 27 d5 3b eb b2 12 4d 8c 0d 62 52 0b 15 9f d1 7a d4 98 0c 9d 01 7e d7 b6 6b,回头看看lol游戏进程获取ob录像chunk部分时数据包的内容开头部分 开头的字节流是85 a0 4a 30 1b 30 27 d5 3b eb b2 12 4d 8c 0d 62 52 0b 15 9f d1 7a d4 98 0c 9d 01 7e d7 b6 6b 7d,相比之下,重复部分是85 a0 4a 30 1b 30 27 d5 3b eb b2 12 4d 8c 0d 62 52 0b 15 9f d1 7a d4 98...,再多找几个录像获取数据的数据包,核对一下 可以确认,lol的ob录像回放协议中,协议头是85 a0 4a 30 1b 30 27 d5,也就是说,tgp下载的ob文件字节流部分,85 a0 4a 30 1b 30 27 d5前面的都不是lol协议的,这开头的01 00 01 00 00 35 88这7个字节为tgp读取识别的协议头部分。最初replay.exe程序读取理解的部分,本身不是lol客户端读取的协议部分。那么,这01 00 01 00 00 35 887个字节分别代表什么含义呢? 那我们开始找85 a0 4a 30 1b 30 27 d5出现的位置即可(关于在数M数据中,查找几个字节所在位置的地方,你最好有比较简便的方法,千万别告诉我你是一行一行看的。) 可以看出如下结果: 第一个在 00001124 部分,前面7个字节是01 00 01 00 00 35 88; 第二个在 000046B3 部分,前面7个字节是02 00 01 00 00 2b 10; 第三个在 000071CA 部分,前面7个字节是01 00 02 00 00 60 48; (wireshark会告诉你当前行所在的位置编号,记得加上所关注字节的偏移位置) 到了这里,根据编程灵(经)感(验),会这么猜想:第二个位置跟第一个位置相差了46B3-1124=358F个位置,也就是3588+7=358F(tgp协议头中,最后两个字节3588 + 协议头长度 7),用第三个跟第二个最比较以确认猜想对不对。71CA-46B3=2B17,即2B10+7符合猜测。 到此,可确认tgp协议头的7个字节中,最后2个字节表示这后续包的长度。但这后续包长度是否仅仅是后面2个字节所标示的,是不是后面3字节,还是4字节?还不能确定。能确定的是,开头的3个字节肯定不会表示后续包长度作用。如果你还是没发现这规律,那可不能怪我了,谁让你当时没有灵(jing)感(yan)呢? 如此一来,可以用更快的方法验证这个猜想,写一段程序,从ob录像文件中,分析tgp协议头,根据后2个字节对应长度的数据包,打印出前面tgp协议头的7字节,再打印后面8个字节(确认是否为85 a0 4a 30 1b 30 27 d5) [1 0 1 0 0 53 136] [133 160 74 48 27 48 39 213 59 235 178 18 77 140 13 98 82 11 21 159 209 122 212] [2 0 1 0 0 43 16] [133 160 74 48 27 48 39 213 54 53 74 75 93 254 127 91 102 147 88 122 160 207 84] [1 0 2 0 0 96 72] [133 160 74 48 27 48 39 213 240 224 50 221 162 250 17 161 77 240 23 76 184 68 252] [2 0 2 0 0 78 80] [133 160 74 48 27 48 39 213 137 33 26 130 55 190 142 64 241 79 180 111 211 4 8] [1 0 3 0 0 125 208] [133 160 74 48 27 48 39 213 156 71 54 190 104 32 173 195 78 15 247 95 154 20 4] [2 0 3 0 0 124 192] [133 160 74 48 27 48 39 213 71 93 96 97 131 246 103 242 117 117 242 122 102 57 252] [1 0 4 0 0 153 128] [133 160 74 48 27 48 39 213 199 1 236 10 13 19 41 186 138 120 76 94 198 235 231] [2 0 4 0 0 126 48] [133 160 74 48 27 48 39 213 161 190 218 133 34 189 202 6 4 210 38 115 68 138 93] [1 0 5 0 0 145 56] [133 160 74 48 27 48 39 213 181 160 40 167 228 7 106 250 239 177 147 143 223 218 189] [2 0 5 0 0 124 208] [133 160 74 48 27 48 39 213 133 89 166 5 196 161 242 20 46 198 139 26 18 52 142] [1 0 6 0 0 180 152] [133 160 74 48 27 48 39 213 199 1 236 10 13 19 41 186 60 43 67 53 87 214 163] [2 0 6 0 0 130 120] [133 160 74 48 27 48 39 213 158 207 13 38 213 94 203 78 214 51 158 92 104 197 112] [1 0 7 0 0 34 32] [133 160 74 48 27 48 39 213 178 213 133 20 42 35 24 186 160 140 25 92 252 210 242] [2 0 7 0 0 140 112] [133 160 74 48 27 48 39 213 46 67 112 101 89 72 41 205 82 117 228 27 204 109 200] [1 0 8 0 0 82 24] [133 160 74 48 27 48 39 213 142 246 116 70 48 41 45 65 120 25 184 155 91 123 174] [2 0 8 0 0 132 128] [133 160 74 48 27 48 39 213 253 10 27 170 202 136 197 234 239 187 196 5 166 215 182] [1 0 9 0 0 132 32] [133 160 74 48 27 48 39 213 63 45 71 158 103 71 129 113 196 148 131 177 29 128 162] [2 0 9 0 0 131 200] [133 160 74 48 27 48 39 213 52 170 14 71 17 205 63 28 76 253 41 134 241 227 200] [1 0 10 0 0 70 0] [133 160 74 48 27 48 39 213 9 8 197 104 10 153 18 70 24 239 30 230 123 22 102] [2 0 10 0 0 151 72] [133 160 74 48 27 48 39 213 208 112 28 134 21 105 39 113 51 90 157 36 215 94 173] [1 0 11 0 0 69 192] [133 160 74 48 27 48 39 213 207 49 23 198 37 84 251 4 153 255 28 239 177 47 13] [2 0 11 0 0 147 232] [133 160 74 48 27 48 39 213 64 115 149 233 120 244 31 210 191 42 252 38 126 32 27] [1 0 12 0 1 2 80] [133 160 74 48 27 48 39 213 186 139 142 5 35 179 166 152 135 234 102 89 109 33 6] [178 161 62 95 113 100 101] [113 58 47 114 56 216 181 49 171 213 150 80 50 222 228 94 34 110 55 170 212 163 177] [2 213 106 162 142 22 91] [8 155 150 15 27 47 191 79 102 112 16 90 168 160 96 244 195 13 72 185 16 118 29] 当tgp协议头的第五个字节不为0时,开始发生了错乱,截取后续包的内容是113 58 47 114 56 216 181 49,并非133 160 74 48 27 48 39 213(即85 a0 4a 30 1b 30 27 d5的十进制),说明tgp最后2字节表示后续包长度不对,应该继续增加,从目前的错误来看,起码是3个字节,即第5、6、7个字节。但如果是3个字节,而第4个字节也一直是0,显然也不合适。我决定将用后面4个字节表示剩余包的长度来检验一次。如下图: 结果都正确的打印了tgp协议头,以及后续8字节的85 a0 4a 30 1b 30 27 d5(对应十进制为:133 160 74 48 27 48 39 213) 至此,tgp协议头的后4个字节已经确认含义了,那么剩余前3个字节中,第一个字节有两种数值01、02,这两个数值的含义也比较好弄明白,这时,我的灵(jing)感(yan),他们分别对应getGameDataChunk、getKeyFrame的两种类型,可以从tgp录像请求的http中加以确认,如下图: 从本地抓取127.0.0.1的通讯包中,找http请求为getGameDataChunk、getKeyFrame的相应包,对照响应body中,字节流跟ob文件分析结果中,对应的是否一致。请求url为getGameDataChunk的,到tgp协议头第一个字节01的字节流中找。请求为getKeyFrame的,到第一个字节02中查找。这里也可以确认无误了。 至此,tgp协议头中,只剩下第2、3个字节不名含义。在汇总录像播放时,请求的所有url,你会发现getGameDataChunk、getKeyFrame请求中,对应的id参数是递增的,如下图: 再根据最初打印的所有tgp协议头7字节内容中,当第1字节一样时,发现第2字节不变,而第3个字节持续递增,这时有灵(jing)感(yan)了吗?第3个字节肯定是url请求中第n个包的含义。第二字节也可能跟第三字节构成uint16类型的总包数的含义。若表示一场游戏中,录像回放系统中,全部数据包分包数只用1字节表示的话,那一共可存储1<<8 -1 也就是255个,数量可能不够,所以,我假设第二、第三字节一起表示录像回放总包的含义。至此,tgp的分包方式中,包头含义已经全部表示明白了。那么,剩下的,就时写个结构体或者map来存储这些字节流即可。 到这里,ob文件分析完成了,对于lol ob server请求的几个url中,只剩下getLastChunkInfo请求没有处理了,你要知道,ob文件最初的几个json字符串中,还剩下chunk_tab、keyframe_tab 聪明的你,这时应该有灵(jing)感(yan)了吧。你来实现实时?什么?还是没灵(jing)感(yan)?再见...... 程序写好后,可以体验一下mac上lol客户端,看国服ob咯,不联网也可以看哦,只要从tgp助手那下载了ob录像文件。 国服客户端,提供观战功能并不能用,每次都是无法观战,或者观战数据加载错误之类的,不提供实时观战功能。ob录像请求的url返回404。 英雄联盟,游戏分为4个主进程 更新进程:LoLpatcher 启动进程:LoLLauncher 客户端(游戏大厅):LolClient 游戏进程:LeagueofLegends 这里已实现mac osx上观看lol ob录像,故可得知mac lol 游戏主进程 可以解析 国服lol 产生的lol协议文件,可大约证明游戏进程是兼容的,甚至一致的。。 更新进程上关系不大,只要保证版本一致即可。启动进程跟客户端两个进程实现了登陆认证功能,要做的,是实现这国服qq登陆认证绑定,也就是说mac osx上玩国服lol 理论上可行....要不,你来试试? 代码开源: 2015-10-27 +3 7 0
不要在linux上启用net.ipv4.tcp_tw_recycle参数 本文为翻译英文BLOG《Coping with the TCP TIME-WAIT state on busy Linux servers》,但并非完整的翻译,译者CFC4N对原文理解后,进行了调整,增加了相关论点论据,跟原文稍有不同。翻译的目的,是为了加深自己知识点的记忆,以及分享给其他朋友,或许对他们也有帮助。文章比较长,没耐心请点关闭。 不要启用 net.ipv4.tcp_tw_recycle linux 内核文档中,对net.ipv4.tcp_tw_recycle的描述并不是很明确。 tcp_tw_recycle (Boolean; default: disabled; since Linux 2.4)[译者注:来自linux man tcp的描述] Enable fast recycling of TIME-WAIT sockets. Enabling this option is not recommended since this causes problems when working with NAT (Network Address Translation). 启用TIME-WAIT状态sockets的快速回收,这个选项不推荐启用。在NAT(Network Address Translation)网络下,会导致大量的TCP连接建立错误。 与其功能相似的参数net.ipv4.tcp_tw_reuse,手册里稍微有点描述,如下: tcp_tw_reuse (Boolean; default: disabled; since Linux 2.4.19/2.6) Allow to reuse TIME-WAIT sockets for new connections when it is safe from protocol viewpoint. It should not be changed without advice/request of technical experts. //从协议设计上来看,对于TIME-WAIT状态的sockets重用到新的TCP连接上来说,是安全的。(用于客户端时的配置) 这里的注释说明非常的少,我们发现,网上很多linux参数调整指南都建议把这些参数net.ipv4.tcp_tw_recycle 设置1「启用」,用于快速减少在TIME-WAIT状态TCP连接数。 但是,在TCP(7)手册中,参数net.ipv4.tcp_tw_recycle 非常蛋疼,尤其是在普通用户家中,有多台设备,或者网吧、公司等多台设备,共用同一个NAT设备环境下,TW回收选项是很有问题的面向公共服务器作为它不会把手连接两台不同的计算机上,这问题很难发现,无从下手。 Enable fast recycling of TIME-WAIT sockets. Enabling this option is not recommended since this causes problems when working with NAT (Network Address Translation). 启用TIME-WAIT状态sockets的快速回收,这个选项不推荐启用。在NAT(Network Address Translation)网络下,会导致大量的TCP连接建立错误。如果没有技术大神的指点的话,千万不要去改动他。 下文将给予更详细的解释,希望可以纠正互联网上错误的观点,尤其是转载比较多的内容,搜索时,往往排在前面,使用者往往接触到的都是不严谨的或者是错误的知识点。 正如此文,在 net.ipv4.tcp_tw_recycle 控制参数中,尽管很多地方写的是ipv4,但对ipv6同样实用。此外,我们这里聊的是Linux TCP协议栈,在linux上可能会受到Netfilter影响,稍微有差异。 关于TCP连接的TIME-WAIT状态,它是为何而生,存在的意义是什么? 让我们回忆一下,什么是TCP TIME-WAIT状态?如下图 这图中的流程不是很好理解,再看一张流程更清晰的图 当TCP连接关闭之前,首先发起关闭的一方会进入TIME-WAIT状态,另一方可以快速回收连接。 可以用ss -tan来查看TCP 连接的当前状态 TIME-WAIT状态的作用 对于TIME-WAIT状态来说,有两个作用 一、人尽皆知的是,防止上一个TCP连接的延迟的数据包(发起关闭,但关闭没完成),被接收后,影响到新的TCP连接。(唯一连接确认方式为四元组:源IP地址、目的IP地址、源端口、目的端口),包的序列号也有一定作用,会减少问题发生的几率,但无法完全避免。尤其是较大接收windows size的快速(回收)连接。RFC1137解释了当TIME-WAIT状态不足时将会发生什么。如果TIME-WAIT状态连接没有被快速回收,会避免什么问题呢?请看下面的例子: 缩短TIME-WAIT的时间后,延迟的TCP 包会被新建立的TCP连接接收。 二、另外一个作用是,当最后一个ACK丢失时,远程连接进入LAST-ACK状态,它可以确保远程已经关闭当前TCP连接。如果没有TIME-WAIT状态,当远程仍认为这个连接是有效的,则会继续与其通讯,导致这个连接会被重新打开。当远程收到一个SYN 时,会回复一个RST包,因为这SEQ不对,那么新的连接将无法建立成功,报错终止。 如果远程因为最后一个ACK包丢失,导致停留在LAST-ACK状态,将影响新建立具有相同四元组的TCP连接。 RFC 793中强调TIME-WAIT状态必须是两倍的MSL时间(max segment lifetime),在linux上,这个限制时间无法调整,写死为1分钟了,定义在include/net/tcp.h 曾有人提议将TCP TIME-WAIT时间改为一个可以自定义配置的参数,但被拒绝了,其实,这对TCP规范,对TIME-WAIT来说,是利大于弊的。 那么问题来了 我们来看下,为什么这个状态能影响到一个处理大量连接的服务器,从下面三个方面来说: 新老连接(相同四元组)在TCP连接表中的slot复用避免 内核中,socket结构体的内存占用 额外的CPU开销 而ss -tan state time-wait|wc -l的结果,并不能说明这些问题。 Connection table slot连接表槽 处于TIME-WAIT状态的TCP连接,在链接表槽中存活1分钟,意味着另一个相同四元组(源地址,源端口,目标地址,目标端口)的连接不能出现,也就是说新的TCP(相同四元组)连接无法建立。 对于web服务器来说,目标地址、目标端口都是固定值。如果web服务器是在L7层的负载均衡后面,那么源地址更是固定值。在LINUX上,作为客户端时,客户端端口默认可分配的数量是3W个(可以在参数net.ipv4.up_local_port_range上调整)。 这意味着,在web服务器跟负载均衡服务器之间,每分钟只有3W个端口是处于established状态,也就大约500连接每秒。 如果TIME-WAIT状态的socket出现在客户端,那这个问题很容易被发现。调用connect()函数会返回EADDRNOTAVAIL,程序也会记录相关的错误到日志。 如果TIME-WATI状态的socket出现在服务端,问题会非常复杂,因为这里并没有日志记录,也没有计数器参考。不过,可以列出服务器上当前所有四元组连接的数量来确认 解决办法是,增加四元组的范围,这有很多方法去实现。(以下建议的顺序,实施难度从小到大排列) 修改net.ipv4.ip_local_port_range参数,增加客户端端口可用范围。 增加服务端端口,多监听一些端口,比如81、82、83这些,web服务器前有负载均衡,对用户友好。 增加客户端IP,尤其是作为负载均衡服务器时,使用更多IP去跟后端的web服务器通讯。 增加服务端IP。 当然了,最后的办法是调整net.ipv4.tcp_tw_reuse和net.ipv4.tcp_tw_recycle。但不到万不得已,千万别这么做,稍后再讲。 内存 保持大量的连接时,当多为每一连接多保留1分钟,就会多消耗一些服务器的内存。举个栗子,如果服务器每秒处理了1W个新的TCP连接,那么服务器将会存货1W/s*60s = 60W个TIME-WAIT状态的TCP连接,那这将会占用多大的内存么?别担心,少年,没那么多。 首先,从应用的角度来看,一个TIME-WAIT状态的socket不会消耗任何内存:socket已经关了。在内核中,TIME-WAIT状态的socket,对于三种不同的作用,有三个不同的结构。 一、“TCP established hash table”的连接存储哈希表(包括其他非established状态的连接),当有新的数据包发来时,是用来定位查找存活状态的连接的。 该哈希表的bucket都包括在TIME-WAIT连接列表以及正在活跃的连接列表中(netstat -antp命令的结果中,没PID的TIME_WAIT状态连接,跟有PID的活跃连接两种)。 该哈希表的大小,取决于操作系统内存大小。在系统引导时,会打印出来,dmesg日志中可以看到。 这个数值,有可能被kernel启动参数thash_entries(设置TCP连接哈希表的最大数目)的改动而将其覆盖。 在TIME-WAIT状态连接列表中,每一个元素都是一个tcp_timewait_sock结构体,其他状态的连接都是tcp_sock结构体。 二、“death row”连接列表,是用来终止TIME-WAIT状态的连接的,这会在他们过期之前,开始申请。它占用的内存空间,跟在连接哈希表中的一样。这个结构体hlist_node tw_death_node是inet_timewait_sock的一个成员,如上代码的倒数第二行。 三、端口绑定的哈希表,用来存储绑定端口跟其他参数,用来确保当前端口没有被使用的,比如在listen监听时的指定端口,或者连接其他socket时,系统动态分配的端口。该哈希表的大小跟连接哈希表大小一样。 每个元素都是inet_bind_socket结构体。每个绑定的端口都会有一个元素。对于web服务器来说,它绑定的是80端口,其TIME-WAIT连接都是共享同一个entry的。另外,连接到远程服务器的本地连接,他们的端口都是随机分配的,并不共享其entry。 所以,我们只关心结构体tcp_timewait_sock跟结构体inet_bind_socket所占用的空间大小。每一个连到远程,或远程连到本地的一个TIME-WAIT状态的连接,都有一个tcp_timewait_sock结构体。还有个结构体inet_bind_socket,只会在连到远程的连接会存在,远程连过来的连接没这个结构体。 tcp_timewait_sock结构体的大小只有168 bytes,inet_bind_socket结构体为48bytes: 所以,当服务器上有4W个连进来的连接进入TIME-WAIT状态时,才用了10MB不到的内存。如果服务器上有4W个连接到远程的连接进入TIME-WAIT状态时,才用了2.5MB的内存。再来看下slabtop的结果,这里测试数据是5W个TIME-WAIT状态的连接结果,其中4.5W是连接到远程的连接: 命令执行结果原样输出,一个字符都没动。TIME-WAIT状态的连接占用内存非常的小。如果你的服务器上要处理每秒成千上万的新建TCP连接,你可能需要多一点的内存才能 正确无误的跟客户端做数据通信。但TIME-WAIT状态连接的内存占用,简直可以无视。 CPU 在CPU这边,查找一个空闲端口的操作,还是蛮珍贵的。这由inet_csk_get_port() 函数,加锁,遍历整个空闲端口列表实现。这个哈希表里条目数量大通常不是问题,如果服务器上存在大量连接到远程TIME-WAIT状态的连接(比如FPM连redis、memcache之类),都会同享相同的profile,这个特性会非常快的按照顺序找到一个新的空闲端口。 其他解决办法 如果你读了上面的章节后,仍对TIME-WAIT状态的连接存有疑问,那么接着看吧: 禁用socket延迟关闭「译者注1:以ubuntu 12.04为例,linger结构体定义在:/usr/src/linux-headers-3.2.0-23/include/linux/socket.h」 禁用net.ipv4.tcp_tw_reuse 禁用net.ipv4.tcp_tw_recycle 当close被调用时,SOCKET需要延迟关闭(lingering),在内核buffers中的残留数据将会发送到远程地址,同时,socket会切换到TIME-WAIT状态。如果禁用此选项,则调用close之后,底层也会关闭,不会将Buffers中残留数据未发送的数据继续发送。 不过呢,应用程序可以选择禁用socket lingering延迟关闭行为。关于socket lingering 延迟关闭,下面两个行为简单描述一下: 第一种情况,close函数后,并不会直接终止该四元组连接序号,而是在buffers任何残留数据都会被丢弃。该TCP连接将会收到一个RST的关闭信号,之后,服务端将立刻销毁该(四元组)连接。 在这种做法中,不会再有TIME-WAIT状态的SOCKET出现。第二种情况,如果当调用close函数后,socket发送buffer中仍然有残留数据,此进程将会休眠,直到所有数据都发送完成并确认,或者所配置的linger计时器过期了。非阻塞socket可以设置不休眠。如上,这些过程都都在底层发生,这个机制确保残留数据在配置的超时时间内都发送出去。 如果数据正常发送出去,close包也正常发送,那么将会转换为TIME-WAIT状态。其他异常情况下,客户端将会收到RST的连接关闭信号,同时,服务端残留数据会被丢弃。 这里的两种情况,禁用socket linger延迟关闭不是万金油。但在HAproxy,Nginx(反代)场景中,在TCP协议上层的应用上(比如HTTP),比较合适。同样,也有很多无可厚非的理由不能禁用它。 net.ipv4.tcp_tw_reuse TIME-WAIT状态是为了防止不相关的延迟请求包被接受。但在某些特定条件下,很有可能出现,新建立的TCP连接请求包,被老连接(同样的四元组,暂时还是TIME-WAIT状态,回收中)的连接误处理。RFC 1323 实现了TCP拓展规范,以保证网络繁忙状态下的高可用。除此之外,另外,它定义了一个新的TCP选项--两个四字节的timestamp fields时间戳字段,第一个是TCP发送方的当前时钟时间戳,而第二个是从远程主机接收到的最新时间戳。 启用net.ipv4.tcp_tw_reuse后,如果新的时间戳,比以前存储的时间戳更大,那么linux将会从TIME-WAIT状态的存活连接中,选取一个,重新分配给新的连接出去的TCP连接。 连出的TIME-WAIT状态连接,仅仅1秒后就可以被重用了。 如何确通讯安全性? TIME-WAIT的第一个作用是避免新的连接(不相关的)接收到重复的数据包。由于使用了时间戳,重复的数据包会因为timestamp过期而丢弃。 第二个作用是确保远程端(远程的不一定是服务端,有可能,对于服务器来说,远程的是客户端,我这里就用远程端来代替)是不是在LAST-ACK状态。因为有可能丢ACK包丢。远程端会重发FIN包,直到 放弃(连接断开) 等到ACK包 收到RST包 如果 FIN包接及时收到,本地端依然是TIME-WAIT状态,同时,ACK包也会发送出去。 一旦新的连接替换了TIME-WAIT的entry,新连接的SYN包会被忽略掉(这得感谢timestramps),也不会应答RST包,但会重传FIN包。 FIN包将会收到一个RST包的应答(因为本地连接是SYN-SENT状态),这会让远程端跳过LAST-ACK状态。 最初的SYN包将会在1秒后重新发送,然后完成连接的建立。看起来没有啥错误发生,只是延迟了一下。 另外,当连接被重用时,TWrecycled计数器会增加的。「译者注:见/proc/net/netstat 中TWrecycled的值」 net.ipv4.tcp_tw_recycle 这种机制也依赖时间戳选项,这也会影响到所有连接进来和连接出去的连接。「译者注:linux上tcp_timestamps默认开启」 TIME-WAIT状态计划更早的过期:它将会在超时重发(RTO)间隔后移除(底层会根据当前连接的延迟状况根据RTT来计算RTO值,上篇《PHP-FPM中backlog参数变更的一些思考》也有提到过,比较复杂的算法)。可以执行ss指令,获取当前存活的TCP连接状态,查看这些数据。「译者注:linux指令ss的结果中rto,rtt值单位均为ms」 Linux将会放弃所有来自远程端的timestramp时间戳小于上次记录的时间戳(也是远程端发来的)的任何数据包。除非TIME-WAIT状态已经过期。 当远程端主机HOST处于NAT网络中时,时间戳在一分钟之内(MSL时间间隔)将禁止了NAT网络后面,除了这台主机以外的其他任何主机连接,因为他们都有各自CPU CLOCK,各自的时间戳。这会导致很多疑难杂症,很难去排查,建议你禁用这个选项。另外,对方上TCP的LAST-ACK状态是体现本机net.ipv4.tcp_tw_recycle的最好数据。 总结 最合适的解决方案是增加更多的四元组数目,比如,服务器可用端口,或服务器IP,让服务器能容纳足够多的TIME-WAIT状态连接。在我们常见的互联网架构中(NGINX反代跟NGINX,NGINX跟FPM,FPM跟redis、mysql、memcache等),减少TIME-WAIT状态的TCP连接,最有效的是使用长连接,不要用短连接,尤其是负载均衡跟web服务器之间。尤其是链家事件中的PHP连不上redis。 在服务端,不要启用net.ipv4.tcp_tw_recycle,除非你能确保你的服务器网络环境不是NAT。在服务端上启用net.ipv4.tw_reuse对于连接进来的TCP连接来说,并没有任何卵用。 在客户端(尤其是服务器上,某服务以客户端形式运行时,比如上面提到的nginx反代,连接着redis、mysql的FPM等等)上启用net.ipv4.tcp_tw_reuse,还算稍微安全的解决TIME-WAIT的方案。再开启net.ipv4.tcp_tw_recycle的话,对客户端(或以客户端形式)的回收,也没有什么卵用,反而会发生很多诡异的事情(尤其是FPM这种服务器上,相对nginx是服务端,相对redis是客户端)。 最后引用一下W. Richard Stevens在《UNIX网络编程》的一句话 The TIME_WAIT state is our friend and is there to help us (i.e., to let old duplicate segments expire in the network). Instead of trying to avoid the state, we should understand it. 译者:存在即合理,勇敢面对,而不是逃避。 译者注: 译者根据原文做了翻译,并增加了相关论点论据,跟原文稍有不同。 linux的netstat命令ss命令的结果中,前者是TIME_WAIT之类,分个字符是下划线,而后者是TIME-WAIT的中划线。 残酷的案例: Clients behind NAT/Stateful FW will get dropped, 99.99999999% of time should never be enabledTuning TCP and nginx on ec2中30页内容 参考资料: Tuning TCP and nginx on ec2 -p30 TIME_WAIT and its design implications for protocols and scalable client server systems tcp_tw_recycle和tcp_timestamps导致connect失败问题 记一次TIME_WAIT网络故障 打开tcp_tw_recycle引起的一个问题 Transmission Control Protocol 2017年10月更新 在linux 4.1内核中,net.ipv4.tcp_tw_recycle参数将被移除。net.ipv4.tcp_tw_recycle has been removed from Linux 4.12. The tcp_tw_recycle was already broken for connections behind NAT, since the per-destination timestamp is not monotonically increasing for multiple machines behind a single destination address. After the randomization of TCP timestamp offsets in commit 8a5bd45f6616 (tcp: randomize tcp timestamp offsets for each connection), the tcp_tw_recycle is broken for all types of connections for the same reason: the timestamps received from a single machine is not monotonically increasing, anymore. Remove tcp_tw_recycle, since it is not functional. Also, remove the PAWSPassive SNMP counter since it is only used for tcp_tw_recycle, and simplify tcp_v4_route_req and tcp_v6_route_req since the strict argument is only set when tcp_tw_recycle is enabled. 2015-07-27 +7 11 0
TCP SOCKET中backlog参数的用途是什么? 在前年时,业务中遇到好多次因为PHP-FPM的backlog参数引发的性能问题,一直想去详细研究一番,还特意在2013年总结里提到这事《为何PHP5.5.6中fpm backlog Changed default listen() backlog to 65535》,然而,我稍于懒惰,一拖再拖,直至今日,才动脑去想,动笔去写。 2013年12月14发布的PHP5.5.6中,changelog中有一条变更, FPM: Changed default listen() backlog to 65535. 这条改动,是在10月28日改的,见increase backlog to the highest value everywhere It makes no sense to use -1 for *BSD (which is the highest value there) and still use 128 for Linux. Lets raise it right to up the limit and let the people lower it if they think that 3.5Mb is too much for a process. IMO this is better than silently dropping connections. patch提交者认为,提高backlog数量,哪怕出现timeout之类错误,也比因为backlog满了之后,悄悄的忽略TCP SYN的请求要好。 当我最近开始想好好了解一下backlog的细节问题时,发现fpm的默认backlog已经不是65535了,现在是511了。(没写在changelog中,你没注意到,这不怪你。)我翻阅了github提交记录,找到这次改动,是在2014年7月22日,Set FPM_BACKLOG_DEFAULT to 511 It is too large for php-fpm to set the listen backlog to 65535. It is realy NOT a good idea to clog the accept queue especially when the client or nginx has a timeout for this connection. Assume that the php-fpm qps is 5000. It will take 13s to completely consume the 65535 backloged connections. The connection maybe already have been closed cause of timeout of nginx or clients. So when we accept the 65535th socket, we get a broken pipe. Even worse, if hundreds of php-fpm processes get a closed connection they are just wasting time and resouces to run a heavy task and finally get error when writing to the closed connection(error: Broken Pipe). The really max accept queue size will be backlog+1(ie, 512 here). We take 511 which is the same as nginx and redis. 其中理由是“backlog值为65535太大了。会导致前面的nginx(或者其他客户端)超时”,而且提交者举例计算了一下,假设FPM的QPS为5000,那么65535个请求全部处理完需要13s的样子。但前端的nginx(或其他客户端)已经等待超时,关闭了这个连接。当FPM处理完之后,再往这个SOCKET ID 写数据时,却发现连接已关闭,得到的是“error: Broken Pipe”,在nginx、redis、apache里,默认的backlog值兜是511。故这里也建议改为511。(后来发现,此patch提交者,是360团队「基础架构快报」中《TCP三次握手之backlog》的作者shafreeck) 对于backlog的参数的含义,在linux kernel手册中也给了注释说明listen - listen for connections on a socket #include /* See NOTES */ #include int listen(int sockfd, int backlog); Description listen() marks the socket referred to by sockfd as a passive socket, that is, as a socket that will be used to accept incoming connection requests using accept(2). The sockfd argument is a file descriptor that refers to a socket of type SOCK_STREAM or SOCK_SEQPACKET. The backlog argument defines the maximum length to which the queue of pending connections for sockfd may grow. If a connection request arrives when the queue is full, the client may receive an error with an indication of ECONNREFUSED or, if the underlying protocol supports retransmission, the request may be ignored so that a later reattempt at connection succeeds. Notes To accept connections, the following steps are performed: 1. A socket is created with socket(2). 2. The socket is bound to a local address using bind(2), so that other sockets may be connect(2)ed to it. 3. A willingness to accept incoming connections and a queue limit for incoming connections are specified with listen(). 4. Connections are accepted with accept(2). POSIX.1-2001 does not require the inclusion of , and this header file is not required on Linux. However, some historical (BSD) implementations required this header file, and portable applications are probably wise to include it. The behavior of the backlog argument on TCP sockets changed with Linux 2.2. Now it specifies the queue length for completely established sockets waiting to be accepted, instead of the number of incomplete connection requests. The maximum length of the queue for incomplete sockets can be set using /proc/sys/net/ipv4/tcp_max_syn_backlog. When syncookies are enabled there is no logical maximum length and this setting is ignored. See tcp(7) for more information. If the backlog argument is greater than the value in /proc/sys/net/core/somaxconn, then it is silently truncated to that value; the default value in this file is 128. In kernels before 2.4.25, this limit was a hard coded value, SOMAXCONN, with the value 128. backlog的定义是已连接但未进行accept处理的SOCKET队列大小,已是(并非syn的SOCKET队列)。如果这个队列满了,将会发送一个ECONNREFUSED错误信息给到客户端,即 linux 头文件 /usr/include/asm-generic/errno.h中定义的“Connection refused”,(如果协议不支持重传,该请求会被忽略)如下: 在linux 2.2以前,backlog大小包括了半连接状态和全连接状态两种队列大小。linux 2.2以后,分离为两个backlog来分别限制半连接SYN_RCVD状态的未完成连接队列大小跟全连接ESTABLISHED状态的已完成连接队列大小。互联网上常见的TCP SYN FLOOD恶意DOS攻击方式就是用/proc/sys/net/ipv4/tcp_max_syn_backlog来控制的,可参见《TCP洪水攻击(SYN Flood)的诊断和处理》。 在使用listen函数时,内核会根据传入参数的backlog跟系统配置参数/proc/sys/net/core/somaxconn中,二者取最小值,作为“ESTABLISHED状态之后,完成TCP连接,等待服务程序ACCEPT”的队列大小。在kernel 2.4.25之前,是写死在代码常量SOMAXCONN,默认值是128。在kernel 2.4.25之后,在配置文件/proc/sys/net/core/somaxconn (即 /etc/sysctl.conf 之类 )中可以修改。我稍微整理了流程图,如下: 如图,服务端收到客户端的syn请求后,将这个请求放入syns queue中,然后服务器端回复syn+ack给客户端,等收到客户端的ack后,将此连接放入accept queue。 大约了解其参数代表意义之后,我稍微测试了一番,并抓去了部分数据,首先确认系统默认参数 在FPM的配置中,listen.backlog值默认为511,而如上结果中看到的Send-Q却是128,可见确实是以/proc/sys/net/core/somaxconn跟listen参数的最小值作为backlog的值。 apache ab这边的结果中,非2xx的http响应有271个,在NGINX日志数据如下: 从nginx结果中看出,本次压测总请求数为10000。http 200响应数量9729个;http 502 响应数量186个;http 504响应数量未85个;即非2xx响应总数为502+504总数,为271个。同时,也跟error.log中数据吻合。同时,也跟TCP数据包中的RST包数量吻合。 在nginx error中,错误号为111,错误信息为“Connection refused”的有186条,对应着所有http 502响应错误的请求;错误号为110,错误信息为“Connection timed out”的有85条,对应着所有http 504响应错误的请求。在linux errno.h头文件定义中,错误号111对应着ECONNREFUSED;错误号110对应着ETIMEDOUT。linux man手册里,对listen参数的说明中,也提到,若client连不上server时,会报告ECONNREFUSED的错。 Nginx error日志中的详细错误如下: 在压测的时候,我用tcpdump抓了通讯包,配合通信包的数据,也可以看出,当backlog为某128时,accept queue队列塞满后,TCP建立的三次握手完成,连接进入ESTABLISHED状态,客户端(nginx)发送给PHP-FPM的数据,FPM处理不过来,没有调用accept将其从accept quque队列取出时,那么就没有ACK包返回给客户端nginx,nginx那边根据TCP 重传机制会再次发从尝试...报了“111: Connection refused”错。当SYNS QUEUE满了时,TCPDUMP的结果如下,不停重传SYN包。 对于已经调用accept函数,从accept queue取出,读取其数据的TCP连接,由于FPM本身处理较慢,以至于NGINX等待时间过久,直接终止了该fastcgi请求,返回“110: Connection timed out”。当FPM处理完成后,往FD里写数据时,发现前端的nginx已经断开连接了,就报了“Write broken pipe”。当ACCEPT QUEUE满了时,TCPDUMP的结果如下,不停重传PSH SCK包。(别问我TCP RTO重传的机制,太复杂了,太深奥了 、TCP的定时器系列 — 超时重传定时器) 对于这些结论,我尝试搜了很多资料,后来在360公司的「基础架构快报」中也看到了他们的研究资料《TCP三次握手之backlog》,也验证了我的结论。 关于ACCEPT QUEUE满了之后的表现问题,早上IM鑫爷给我指出几个错误,感谢批评及指导,在这里,我把这个问题再详细描述一下。如上图所示 NO.515 client发SYN到server,我的seq是0,消息包内容长度为0. (这里的seq并非真正的0,而是wireshark为了显示更好阅读,使用了Relative SeqNum相对序号) NO.516 server回SYN ACK给client,我的seq是0,消息包内容长度是0,已经收到你发的seq 1 之前的TCP包。(请发后面的) NO.641 client发ACK给server,我是seq 1 ,消息包内容长度是0,已经收到你发的seq 1 之前的TCP包。 NO.992 client发PSH给server,我是seq 1 ,消息包内容长度是496,已经收到你发的seq 1 之前的TCP包。 ...........等了一段时间之后(这里约0.2s左右) NO.4796 client没等到对方的ACK包,开始TCP retransmission这个包,我是seq 1,消息包长度496,已经收到你发的seq 1 之前的TCP包。 ..........又...等了一段时间 NO.9669 client还是没等到对方的ACK包,又开始TCP retransmission这个包,我是seq 1,消息包长度496,已经收到你发的seq 1 之前的TCP包。 NO.13434 server发了SYN ACK给client,这里是tcp spurious retransmission 伪重传,我的seq是0,消息包内容长度是0,已经收到你发的seq 1 之前的TCP包。距离其上次发包给client是NO.516 已1秒左右了,因为没有收到NO.641 包ACK。这时,client收到过server的SYN,ACK包,将此TCP 连接状态改为ESTABLISHED,而server那边没有收到client的ACK包,则其TCP连接状态是SYN_RCVD状态。(感谢IM鑫爷指正)也可能是因为accept queue满了,暂时不能将此TCP连接从syns queue拉到accept queue,导致这情况,这需要翻阅内核源码才能确认。 NO.13467 client发TCP DUP ACK包给server,其实是重发了N0.641 ,只是seq变化了,因为要包括它之前发送过的seq的序列号总和。即..我的seq 497 ,消息包内容长度是0,已经收到你发的seq 1 之前的TCP包。 NO.16573 client继续重新发消息数据给server,包的内容还是NO.992的内容,因为之前发的几次,都没收到确认。 NO.25813 client继续重新发消息数据给server,包的内容还还是NO.992的内容,仍没收到确认。(参见下图中绿色框内标识) NO.29733 server又重复了NO.13434包的流程,原因也一样,参见NO.13434包注释 NO.29765 client只好跟NO.13467一样,重发ACK包给server。 NO.44507 重复NO.16573的步骤 NO.79195 继续重复NO.16573的步骤 NO.79195 server立刻直接回了RST包,结束会话 详细的包内容备注在后面,需要关注的不光是包发送顺序,包的seq重传之类,还有一个重要的,TCP retransmission timeout,即TCP超时重传。对于这里已经抓到的数据包,wireshark可以看下每次超时重传的时间间隔,如下图: RTO的重传次数是系统可配置的,见/proc/sys/net/ipv4/tcp_retries1 ,而重传时间间隔,间隔增长频率等,是比较复杂的方式计算出来的,见《TCP/IP重传超时--RTO》。 backlog大小设置为多少合适? 从上面的结论中可以看出,这跟FPM的处理能力有关,backlog太大了,导致FPM处理不过来,nginx那边等待超时,断开连接,报504 gateway timeout错。同时FPM处理完准备write 数据给nginx时,发现TCP连接断开了,报“Broken pipe”。backlog太小的话,NGINX之类client,根本进入不了FPM的accept queue,报“502 Bad Gateway”错。所以,这还得去根据FPM的QPS来决定backlog的大小。计算方式最好为QPS=backlog。对了这里的QPS是正常业务下的QPS,千万别用echo hello world这种结果的QPS去欺骗自己。当然,backlog的数值,如果指定在FPM中的话,记得把操作系统的net.core.somaxconn设置的起码比它大。另外,ubuntu server 1404上/proc/sys/net/core/somaxconn 跟/proc/sys/net/ipv4/tcp_max_syn_backlog 默认值都是128,这个问题,我为了抓数据,测了好几遍才发现。 对于测试时,TCP数据包已经drop掉的未进入syns queue,以及未进入accept queue的数据包,可以用netstat -s来查看: 经过相关资料查阅,技术点研究,再做一番测试之后,又加深了我对TCP通讯知识点的记忆,以及对sync queue、accept queue所处环节知识点薄弱的补充,也是蛮有收获,这些知识,在以后的纯TCP通讯程序研发过程中,包括高性能的互联网通讯中,想必有很大帮助,希望自己能继续找些案例来实践检验一下对这些知识的掌握。 参考资料 TCP三次握手之backlog linux里的backlog详解 tcp-queue-的一些问题 The meaning of listen(2)'s backlog parameter TCP 的那些事儿(上) TCP/IP重传超时--RTO TCP的定时器系列—超时重传定时器 Coping with the TCP TIME-WAIT state on busy Linux servers 2018年10月22日新增How TCP backlog works in Linux 2015-06-02 +8 12 0
nginx、php-fpm默认配置与性能–TCP socket还是unix domain socket 前几天看到一篇博客,提到php所在服务器在大并发情况下,频繁创建TCP短连接,而其所在服务器的2MSL时间过长,导致没有端口可用,系统无法创建TCP socket,而大量报错。博主在后面给的解决方案是减少2MSL的时间,尽快清除TIME_WAIT状态的TCP连接,回收端口。同时,文章结尾写了不用长连接的理由,但这真的是最好的解决办法吗?有其他办法可以更好的做法吗? 类似经历 之所以多这篇文章兴趣这么高,是因为在前段时间,末学也经历了一件类似的优化历程,先简短的描述下我们的服务器架构。如图 想必看到这幅架构图的同学,都比较熟悉吧,也比较简单。“最前面”的nginx 反代负责与玩家的http请求通讯,这里是长连接。在其与后端游戏大区通讯时,使用了短连接,也就是意味着,每处理用户的一个http请求,都要重新与后端的nginx建立一次TCP(http)请求,后端nginx处理完之后就关闭。后端的nginx与php在同一台服务器上,通讯配置如下: 在这个架构图中,反代承担这承上启下的作用,前面是用户,后面是web app服务器,长连接用户,短连接后端。若用户数在1W人,1W人与反代的连接,在反代服务器上都用80端口。但这1W人的每一个请求,反代都要重新使用服务器的端口与后端nginx的80创建tcp socket,来处理请求。若2MSL时间为1分钟以上,同时,玩家两次请求间隔很短,想我们游戏的服务器,两次请求间隔大约5秒(甚至2-3秒)。那么反代的端口,5秒用1W个,10秒2W,15秒3W,20秒4W,25秒5W,30秒6W!!!,前面已经用掉的端口还没被回收掉。那么将会有很多的用户请求被nginx反代接收之后,因没有端口资源而无法与后端nginx创建tcp连接。。。以游戏的其中一个大区为例: 关于这个问题,末学在新浪微博上请教过其他大牛,其中tengine负责人淘叔度前辈以及@120斤的大青蛙前辈告诉我,nginx将在1.1.4版本开始支持ngx_http_upstream_keepalive,3月1号左右,是1.1.16 dev版本,我们的运维团队稍微由于几天,之后在部分小区的服务器上适用了1.1.16,效果不错,Nginx反代上与目标服务器的80端口的TCP连接中处于ESTABLISHED状态的非常多,TIME_WAIT的几乎没有,很稳定。我们几乎计划为了这个性能的提升,决定用dev版了。接着nginx1.2Stable version也出来了,那么也就用这个版本了。至于反代上nginx与后端app server建立TCP连接个数,也就是端口数(满载情况下,维持ESTABLISHED状态)的为nginx worker_processes * ((upstream1 server * keepalive参数数量) + (upstream2 server * keepalive参数数量) + ...),并不多。 如上,端口占用数少了,那么还有其他有点吗?先看一副 socket创建,监听、数据接收发送、关闭与客户端连接的socket流程图: 如上图,nginx反代跟后端nginx的形式跟php-fpm与mysqld socket的模型一样的。 nginx反代跟php-fpm 相对来说,就是客户端,这种做法,他们的每个新socket 请求的发起,都会跟着左边的流程走一遍,一遍又一遍。。。。每次的socket创建,端口等资源申请,与服务端的三次握手,关闭时的四次握手,端口的回收。。。。 改用长连接之后,如下图: 只有php-fpm(或者nginx反代)的子进程分别与mysqld创建1次TCP连接,之后都是send、recv数据的事情了,端口占用也是为数不多的几个(每个fpm子进程将与mysqld维持一个TCP长连接)。 同样,后端服务器的nginx与php-fpm的通讯也是如此,只是请求两没有反代那么大。区别就是IP地址是回环地址127.0.0.1。 既然是回环地址,那么两个服务都是在同一台机器上跑的,既然是同一台机器,为何不用进程间通讯的socket--unix domain socket呢? socket是神马?摘抄一段描述: Socket 可以被定义描述为两个应用通信通道的端点。一个 Socket 端点可以用 Socket 地址来描述, Socket 地址结构由 IP 地址,端口和使用协议组成( TCP or UDP )。http协议可以通过socket实现,socket在传输层上实现。从这个角度来说,socket介于应用层和传输层之间。但是socket作为一种进程通信机制,操作系统分配唯一一个socket号,是依赖于通信协议的,但是这个通信协议不仅仅是 tcp或udp,也可以是其它协议。 在同一台服务器上,用tcp socket与unix domain socket有什么区别? 如图所示,对于进程间通讯的两个程序,unix domain socket的流程不会走到TCP 那层,直接以文件形式,以stream socket通讯。如果是TCP socket,则需要走到IP层。 对于非同一台服务器上,TCP socket走的就更多了。 至于localhost\127.0.0.1以及网络IP他们之间的区别,无意中找到一篇博客写的是以mysql作为验证,来说明localhost不走TCP/IP层,跟127.0.0.1不一样。末学认为他理解错了。他的理由如下 (以下截图均在linux上,windows的没有unix domain socket) mysql连接本机时,不加-h参数: mysql连接本机时,加-h参数且值是localhost: mysql连接本机时,加-h参数且值是127.0.0.1: 那位同学从mysql工具的使用方法、与结果的区别,来理解推导localhost与127.0.0.1的区别,这从方向上就存在问题,我更相信,这是mysql这个程序自己的行为,遇到-h参数没加,或者-h参数的值不是IP形式,且my.cnf里指定mysql的socket路径时,则直接使用unix domain socket来连接服务器,当然,这也是我的猜测,没有去验证,大家听听就好,别相信。 鉴于末学对以上的理解,将服务器的架构配置变更如下 至此,优化还为完毕,若php-fpm与mysql使用mysql_pconnect的话,那么php-fpm的子进程生成模式最好用static模式,若为dynamic模式,可能会出现mysql连接数被占满的情况,这也跟mysql服务的连接超时时间有关,适当调整也容易避免。 不过,我们目前还没用mysql_pconnect,主要原因是我们的代码中,有些事务处理开启之后,对于代码的失败处理,忘记写回滚语句,在短连接的情况下,这个连接的销毁,哪怕客户端没提交ROLLBACK或者COMMIT指令,mysql会自动回滚之前的事务。但使用长连接之后,不同请求会使用同一个MYSQL连接句柄,每个事务开启都会禁用MYSQL的自动提交,即SET AUTOCOMMIT=0语句,这语句会提交之前的事务。对于我们代码忘记写回滚,而直接返回结果的情况下,这是会出大问题的,也是我们目前唯一没有使用MYSQL_pconnect的原因。(计划近期找到没有写回滚语句的代码,修复,继续使用mysql_pconnect) 其实还有,我们php-fpm使用了APC来缓存php file,以及 变量数据等,这些也是有优化的地方(如果有时间的话,则待续)。 回过头来再理解下文章开头那位同学给的解决办法,我仍不能从他给的理由中,理解长连接的缺点,哪怕是解决了TIME_WAIT的问题,但每次创建TCP socket ,连接到服务器时三次握手,关闭TCP socket时的四次握手 这些也是开销。当然,缩短2MSL的时间,也是更好利用服务器资源的一个好方法。 最后,我们调整优化的服务器架构图如下: 好像有点偏离这篇文章的标题了,其实我更想说我不能理解为啥nginx跟php-fpm给的默认配置中,都是TCP socket通讯的,为啥不默认给unix domain socket的默认配置呢?如果说为了方便非同一台服务器时的情况,但给的默认IP也是回环地址呀。 而且,nginx给默认配置中,对于uri请求中的php文件的处理,匹配规则还是老的,之前发生因为NGINX与PHP的配置而导致的安全问题,虽然不是nginx的错,但nginx也可给出更严谨的范例,但仍没有。 值得欣慰的是,在UBUNTU 12.4中,nginx的默认配置有了很大的改进,不管是匹配uri的规则,还是nginx与php-fpm的交互方式: PS:末学只是个web程序员,这些只是末学学习研究如上知识的总结,理解上难免有错,各位请海涵。 2012-05-31 +18 22 0
YSLOW法则中,为什么yahoo推荐用GET代替POST? 背景: 上上周五,公司前端工程师培训,提到前端优化的一些技巧,当然不能少了yahoo yslow的优化法则。其中有这么一条“Use GET for AJAX Requests”,这些法则从最开始的14条,到现在的35条,一直都时刻关注的。可这么一条的原因我却一点都不清楚。在提问的环节里,我对yahoo WEB前端优化法则推荐AJAX中,使用GET代替POST的原因有疑问,便请教前端工程师。我们的工程师说GET的话,浏览器发送一个包,POST会发两个等等。我对这个解释仍带有疑问,甚至怀疑。培训结束后,我随便搜索了一下,并没有得到理想的结果,可能很少人对Yahoo这么有权威的组织提出的优化法则产生怀疑,也很少人想知道为什么建议这么做,更多的人会唯命是从,墨守成规。之后,我又看了遍优化法则,看到一条是推荐开发者使用AJAX缓存的,这时,一个“伟大”的想法在我脑袋中一闪,莫非是GET请求可以缓存,而POST不可以?接着,我把我这个“伟大”的猜测告诉我的同事们,当初已经是下班时间,好多同事都离开公司,我也匆忙收拾东西下班了,没有仔细查找答案。 周末期间,脑袋中频繁的闪现这个问题,仍对我的想法有怀疑,Yahoo前端这么牛X的团队的想法,岂是我这样的菜鸟能这么容易的猜测推断到的?我对我当初的推测的怀疑就像“小时候就怀疑小JJ绝对不是只用来撒尿那么简单”一样坚定。但向我这么懒惰的同学,实在找不出一点时间来验证我这个想法,空闲的时间宁愿多打几盘CS。一直拖到现在,台风来了,在家宅两天,头都睡扁了,也找不出不写这篇文章的理由。 验证Yahoo推荐的理由: 验证XHR请求中yahoo推荐用GET代替POST做法的理由 POST is implemented in the browsers as a two-step process: sending the headers first, then sending data. So it's best to use GET POST请求分两步:发送http headers,再发送http data HTML+JS代码: 抓包工具:wireshark 提示:wireshark(1.2.5版)在抓http包的时候,会默认合并packet reassembly选项,记得全部去掉。如下图(edit-->Preferences) 我分别发了一个GET、一个POST的XHR(XMLHttpRequest)请求,其数据包如下: 如上图,GET请求发送的数据包为第一个红框内的结果;POST请求发送的数据包为第二个红框内结果,但多了一个第12条数据包(粉红色框内),从10.0.0.108(我的PC)发往98.126.129.106(www.cnxct.com的服务器IP,也就是表单提交的目标服务器IP),wireshark给出的信息是“Continuation or non-HTTP traffic”,这个提示就是说,本次数据包是接着上一次的HTTP请求发的,没有HTTP header,只有http data。 详情如下图 XHR HTTP POST请求的header部分数据: XHR HTTP POST 请求的DATA部分: 结论?: 果然,如伟大的YAhoo前端团队所说,XHR HTTP的POST请求会分为两步,先发HTTP HEADER,再发HTTP DATA部分。 然而,新的疑问又来了。为什么要分为两部?谁(例如W3C这种机构)规定的?每个浏览器都是这样的么?分两次比一次的的效率更高吗? 继续: 带这我新的疑问,又进行了如下尝试:先分浏览器,IE8、Firefox5.0、Chrome13分别发送XHR GET 、XHR POST请求,抓包对比结果。 我惊奇的发现(细心的同学会注意到第三张图中,有椭圆形的框标出那些结果的浏览器是Chrome13),Firefox5发送POST的数据包确是没有像yahoo前端优化法则中提到的那样,分为两次,两个包发送,而是一次完成http headers和 http data的发送。如下图: 大家可以从图中看到line-based text data:application/x-www-from-urlcoded下面就是POST的数据。 这时,又有很多疑问产生了,其他浏览器呢?IE的所有版本都会分两次发么?Firefox的其他版本呢? 当我想一个一个尝试抓包对比的同时,幸运的搜到了关于我这个疑问的PDF(Analysis_of_browser_specific_characteristics.pdf) 其中,提到firefox大部分版本在XP、WIN7、UBUNTU、MAC OS等系统上都是以1个包来实现的,其他常见浏览器都是分为两个包。 相比大家很清楚的知道,HTTP(TCP)完成一次事务,通讯次数越多,越有可能出现故障(网络延迟等因素),开销越大,浏览器(客户端)、服务端都要再进行一次TCP通讯,而且,需要一定的时间。对于我们追求更高的用户体验,需要HTTP通讯都避免到这些缺点,而各大浏览器开发商为何仍这么做呢?firefox的做法是最好的吗? 上面的PDF里,模拟了各种网络环境,比如网络延迟、网络丢包等情况,分别来对比POST请求的1次包和2次包的优缺点。得出的结论是:当网络环境好的情况下,1次包跟2次包的在时间上差别基本可以无视。而在网络环境差的情况下,(2次包)TCP的验证数据包完整性上,有非常大的优点,客户端先告诉服务端即将发送的数据包大小,MD5等标识,当服务端告诉客户端收到(ACK包)的时候,客户端再次向服务端发送POST 的DATA。假如网络环境不好,网络延迟、丢包的时候,服务端会等待(延迟时),客户端重发POST的DATA数据到服务单,来确保本次请求的完整性。 撰写这个PDF的作者在他的博客里详细的描述了写这个博客的起因,以及结果,还有一些关于与yahoo yslow前端团队的一些沟通过程,大家可以在这里阅读下(yahoo 的前端团队好像不太友好,哈)。 结论: 通过参考这个PDF,以及我自己做的抓包测试,让我了解Yahoo YSLOW前端团队的这个推荐(他们没详细的说为啥这么推荐,只是简单的提了下GET请求产生TCP一个包;POST请求,产生2个TCP包。甚至都没告诉我们Firefox的多数版本[可能是所有版本]都是发一个TCP包的。更详细、更深层原因也没说,这里还得感谢下http://loadimpact.com的作者)。 备注: 这里提供下我抓包测试时候的数据包(截图用到的数据包中包含我的一些cookie,没上传。这里的是我新抓的,各位见谅),各位可以参考下,如果我的文字、方向有错,欢迎指出。 IE8、FF5、Chrome13发起XHR请求数据包 当你读到这里时,我承认,我骗了你,文章的内容不光是标题中所写的,为何推荐使用POST代替GET,更多的是抓去TCP、HTTP通讯包来验证各个浏览器是否如YSLOW所述的那样分2次包的过程,以及2次包与1次包的优缺点(PDF中)。希望你看到最后的时候,忘记标题讲的是什么。 更新日期:2014年10月27日 在网上看到一篇文章也是介绍GET跟POST区别的,见:XMLHttpRequest (XHR) Uses Multiple Packets for HTTP POST? 2011-08-07 +14 18 0