本文纯粹作为学习淘宝褚霸关于 gen_tcp 所分享内容的摘录,方便记忆和后续翻阅。原文详细内容请移步 这里 。
========= 我是美丽的分割线 ===========
0.《 Erlang gen_tcp相关问题汇编索引 》
1.《 Erlang版TCP服务器对抗攻击解决方案 》
本文列举了 TCP 服务器各种可能被攻击的情况,有网友给出了可能的解决办法。
2.《 gen_tcp接受链接时enfile的问题分析及解决 》
知识点:
a.确定 EMFILE 的含义;
b.查看系统本身资源设置情况;
1 2 3 |
|
c.通过 stap 脚本验证是否为操作系统本身的问题;
d.通过 gdb attach 到相应进程查看运行时设置
1 |
|
e.给出 Erlang 服务器性能调优的几个关键的参数,参考 http://www.ejabberd.im/tuning 。
3.《 gen_tcp连接半关闭问题 》
知识点: 当 tcp 对端调用 shutdown(RD/WR) 时候, 宿主进程默认将收到 {tcp_closed, Socket} 消息,导致宿主端 socket 被强制关闭,可以通过设置 inets:setopts(Socket, [{exit_on_close, false}]). 来避免。
4.《 gen_tcp容易误用的一点解释 》
知识点:产生 {error,einval} 错误的原因是由于 {active, true} 和 gen_tcp:recv 混用。
5.《 未公开的gen_tcp:unrecv以及接收缓冲区行为分析 》
知识点:
a.分析了 gen_tcp 接收缓冲区的工作原理以及影响大小的因素,还顺便介绍了 unrecv 的用途。
b.推荐阅读 misultin(小型的 erlang web 服务器)中对于 socket 的处理,里面基于 packet 类型和 active 模式,并利用 erts 已有的协议进行包分析。
6.《 gen_tcp如何限制封包大小 》
介绍在主动模式和被动模式两种情况下对封包的处理原则:
a.当 {active, false} + {packet, raw} 情况下,通过 gen_tcp:recv(Socket, Length) 中的 Length 来对封包大小进行限制,若 Length 为 0,则等同于由对端来对封包大小进行限制。同时 gen_tcp:recv 所使用的接收缓冲区存在最大限制,即 TCP_MAX_PACKET_SIZE(64M)。
b.在 {active, true} 情况下,默认不限制包长度。但可以通过 inet:setopts(Socket, Options) 中的 {packet_size, Integer} 选项进行控制。
文档说明如下:
{packet_size, Integer}(TCP/IP sockets) Sets the max allowed length of the packet body. If the packet header indicates that the length of the packet is longer than the max allowed length, the packet is considered invalid. The same happens if the packet header is too big for the socket receive buffer.
For line oriented protocols (line,http*), option packet_size also guarantees that lines up to the indicated length are accepted and not considered invalid due to internal buffer limitations.
7.《 gen_tcp调用进程收到{empty_out_q, Port}消息奇怪行为分析 》
知识点:
a.描述了 erts 内部 inet_drv 工作原理,其中涉及数据暂存到 port 驱动发送队列的情况,以及对 gen_tcp:close 或者 gen_tcp:shutdown 行为的影响。
b.促使我梳理了一遍 socket 相关代码处理的层次。
c.指出容易触发收到 {empty_out_q, Port} 消息的条件:a) 对端先关闭的时候;b) 我端是被动接收;c) socket 打开 {exit_on_close, true} 和 {delay_send,true} 这二个选项的时候最容易发生。
d.最后给出了能够产生该现象的测试程序(值得学习);ss 命令的使用。
8.《 gen_tcp发送缓冲区以及水位线问题分析 》
知识点:
a.erlang 中拥有消息队列的实体:a) 进程; b) port 。并且 erlang VM 对 port 和 erlang 进程一视同仁的进行公平调度的。
b.我们知道 ! 符号是 erlang:send 的语法糖,当执行 Port ! msg 或者 Pid ! msg 时,最终都是调用 erlang:send 来发送消息。但在后续版本中,erlang 的设计者专门为 port 设计了 erlang:port_command 系列函数用于 port 上发送消息。
erlang:send 的调用层次(本人整理):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
c.水位线的作用:和每个消息队列一样,为了防止发送者和接收者能力的失衡,通常都会设置高低水位线来保护队列不至于太大把系统撑爆。{high_watermark, Size},{low_watermark, Size} 就是干这个用的。水位线的特点:a) 水位线设置是可以继承的;b) 高低水位线默认是 8K/4K ;c) 进入高水位后,port 进入 busy 状态;当消息消耗到小于低水位线,busy 解除。
d.port 的行为:当消息量达到高水位线的时候,port 进入 busy 状态,这时候会把发送进程 suspend 起来,等消息达到低水位线的时候,解除 busy 状态,同时让发送进程继续执行。
e.一个 port 进入 busy 状态会产生什么样的问题?这个状态通常很严重,发送进程被挂起,会引起很大的 latency 。可以通过 erlang:system_monitor(MonitorPid, Options) -> MonSettings 来获取发生 busy_port 的进程,从而知道哪个进程进程由于碰到高水位线被挂起了,方便后续调整水位线避免这种情况的发生。
f.tcp_sendv 的行为:
a)检查 port 驱动队列中是否存在数据,若已存在数据,则直接将当前数据放入 port 驱动队列尾部,在判定总数据长度是否超出 port 驱动队列的高水位线,若超出高水位线,则设置当前 tcp_descriptor 中的相应值,以表明当前 port 已处于 busy 状态,同时记录下 busy_caller 对象,用于后续回复。若未超出,则什么也不做,仅通过 inet_reply_ok 告之 {inet_reply, S, ok} 。而 port 驱动队列中的数据,则会等到 epoll 发现有可写事件时,通过调用 tcp_inet_drv_output 来发送(内部还是调用 sock_sendv)。
b)若检测 port 驱动队列时发现原本无数据,则先检测是否设置了 delay_send 标志,若设置了,则不去调用 sock_sendv 发送,而是之间调用 driver_enqv 将数据放入 port 驱动队列后,再执行 sock_select(...,(FD_WRITE|FD_CLOSE),...) 来检测可写事件。若未设置 delay_send 标志,则调用 sock_sendv 进行数据发送,若数据被全部发送出去,则通过 inet_reply_ok 告之 {inet_reply, S, ok} ;否则通过调用 driver_enqv(ix, ev, n) 来跳过成功写出去的字节数(即下次从剩下的字节位置开始发送)。最后执行 sock_select(...,(FD_WRITE|FD_CLOSE),...) 来检测可写事件。
g.delay_send 的行为:在第一阶段不尝试发送数据,而是直接把数据推入 port 的消息队列去,等后面 epoll 说 socket 可写的时候再一起发送出去。好处是 gen_tcp:send 马上就可以返回,因为 sock_send 通常要耗费几十 us 的时间,可用在对发送的 latency 很敏感的场合。
h.port 中的高低水位线:当执行 gen_tcp:send 而数据无法通过网络发送出去的时候,会暂时保留在 port 的消息队列里面,当消息队列满(到高水位线)的时候,port 就会 busy,抑制发送者推送更多的数据。当 epoll 探测到 socket 可写的时候,vm 会调用 tcp_inet_output 把消息队列里面的数据,发送到网络上去。在这个过程中,队列里面的数据会越来越少,少到低水位线的时候,则解除 port 的 busy 状态,好让发送者发送更多的数据。
i.新的 VM 引入了 port 的 parallelism(R16B的发布note里面和port相关的重大变化)。而 port 的并行发送行为默认是关闭的(为了和过去的版本兼容),但是可以用 +spp 全局打开;在 open_port 的时候可以通过参数 {parallelism, true} 来个别打开这个选项。
j.msgq_watermark 的产生原因:前面分析过,每个 port 的消息队列都有高低水位线来控制,总能保证消息在一定的量。但在开启了 parallelism 了后,当 port 在忙着做 call_driver_outputv 的时候,其他进程就不等了,直接把消息加引用计数保存到一个地方去,然后请求 port 调度器稍后调度执行这个消息,它就立即返回了。如果不做控制的话,每个进程都会积累很多消息,都等着 port 调度器后续执行。所以 port 调度器就有义务来为这部分消息做水位线的控制,这就很自然的引入了 msgq_watermark 选项。
9.《 gen_tcp接收缓冲区易混淆概念纠正 》
知识点:
a.Erlang 的每个 TCP 网络链接是由相应的 gen_tcp 对象来表示的,说白了就是个 port 。
b.给出 gen_tcp 接收包的流程:并给出 INET_LOPT_BUFFER 值选取的依据。
c.inets:getstat(Socket, [recv_avg]). 可以帮统计到平均包大小。
10.《 gen_tcp发送进程被挂起起因分析及对策 》
知识点:
a.如果系统中有大量的 tcp 链接要发送数据,基于 gen_tcp:send 默认行为这种方式有点低效。所以很多系统把这个动作改成集中提交数据,集中等待回应。例如在 rabbit_writer.erl 中的实现。在正常情况下,这种处理会大大提高进程切换的开销,减少等待时间。但是也会带来问题,我们看到 port_command 这个操作如果出现意外,被阻塞了,那么这个系统的消息发送会被卡死。
b.gen_tcp 的默认高低水位线分别为 8K/4K 。可以通过 inet:getopts(Sock,[high_watermark, low_watermark]). 获取当前的设置。
c.erlang 的 tcp port 驱动是支持 ERL_DRV_FLAG_SOFT_BUSY 的。其含义如下
Marks that driver instances can handle being called in the output and/or outputv callbacks even though a driver instance has marked itself as busy (see set_busy_port()). Since erts version 5.7.4 this flag is required for drivers used by the Erlang distribution (the behaviour has always been required by drivers used by the distribution).
d.在 do_port_command 代码中可以看到:a) 如果设置了 force 标志,但是 port 驱动不支持 ERL_DRV_FLAG_SOFT_BUSY,要返回 EXC_NOTSUP 错误;而 elrang 的 port 驱动支持 ERL_DRV_FLAG_SOFT_BUSY 的,所以如果 force 的话,数据会被写入缓冲区;b) 如果设置了 NOSUSPEND,但是 port 已经 busy 了,则返回 false,表明发送失败。若未设置 NOSUSPEND,就把发送进程 suspend,同时告诉 system_monitor 系统现在有 port 进入 busy_port 了。
e.port 是和进程一样公平调度的。进程是按照 reductions 为单位进行调度的,port 是把发送的字节数折合成 reductions 来调度的。所以如果一个进程发送大量的 tcp 数据,那么这个进程不会一致得到执行,运行期会强制其停止一段时间, 让其他 port 有机会执行。
f.port 的调度时间片是从宿主的进程的时间片里面扣的。每个读写占用 200 个时间片,而每个进程初始分配 2000 个时间片,也就是说做 10 次输出就要被调度了。因为 gen_tcp 在发送数据时需要占用宿主进程的 reductions,这也可能造成宿主进程被挂起,在设计的时候尽量避免一个进程拥有太多的 port 。
g.如果 gen_tcp 发送进程不能阻塞,那么就添加 force 标志,强行往缓冲区加入数据,同时设置 {send_timeout, Integer} ;如果该 socket 在指定的时间内无法把数据发送完成,那么就直接宣告 socket 发送超时,避免了潜在的 force 加数据造成的缓冲区占用大量内存而出现问题。
上面分析过,gen_tcp 数据的发送需要占用宿主进程的 reds,这也可能造成宿主进程被挂起,在设计的时候尽量避免一个进程拥有太多的 port.
11.《 gen_tcp:send的深度解刨和使用指南(初稿) 》
a. gen_tcp:send 调用层次(本人整理):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
在 inet_drv 内部会为每个 socket 都设置一个消息队列,用于保持上层推来的消息。这个消息队列有上下水位线。当消息的字节数目超过了高水位线的时候,inet_drv 就把 socket 标志为 busy。这个 busy 要到队列的字节数少于低水位线的时候才解除。
关键数据结构:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
|
b.可以在 prim_inet:send 的实现中看到,当执行 erlang:port_command 时返回 true 后,其后续使用了 receive 来获取来自 ERTS 的反馈,此处为同步动作,对发送大量的消息的场合很不利。更好的做法是手工把 gen_tcp 的 2 个步骤分开做,a) 不停的 erlang:port_command/3 且最好加上 force 标志;b) 被动等待 {inet_reply,S,Status} 消息。具体实现可参考 hotwheels 或者 rabbitmq 项目的代码。
c.分析 erlang:port_command 返回 false 的原因:a) port 驱动不支持 soft_busy, 但是我们用了 force 标志;b) port 驱动已经 busy 了, 但是我们不允许进程挂起。
d.分析 tcp:send 在虚拟机执行这个层面上,调用者进程被挂起的几种可能原因:a) 数据成功推到 ERTS, 等待 ERTS 的发送结果通知,这是大多数情况;b) 该 socket 忙, 并且我们没有设定 port_command 的 force 标志;c) 调用者进程发送了大量的数据,时间片用完被运行期挂起。失败的可能原因:我们设定了 nosuspend 标志,但是 socket 忙。
12.《 Erlang open_port极度影响性能的因素 》
知识点:
a.Erlang 的 port 相当于系统的 I/O,打开了 Erlang 世界通往外界的通道,可以很方便的执行外部程序。
b.open_port 一个外部程序的时候流程大概是这样的:beam.smp 先 vfork,子进程调用 child_setup 程序做进一步的清理操作。清理完成后才真正 exec 我们的外部程序。
c.在支持 vfork 的系统下,比如说 linux,除非禁止,默认会采用 vfork 来执行 child_setup 来调用外部程序。而执行 vfork 的时候 beam.smp 整个进程会被阻塞,所以这里是个很重要的性能影响点。
参考 vfork 文档:
1 |
|
d.在 erl_child_setup.c 代码中可以看到存在遍历所有打开句柄并将其关闭的的动作,而对于一个繁忙的 I/O 服务器来讲,会打开大量的句柄,可能都有几十万,关闭这么多的句柄会是个灾难。
e.设计个 open_port 的场景,服务器打开 768 个 socke 句柄,再运行 cat 外部程序。并使用 stap 进行性能分析。
1 2 3 4 5 6 7 8 9 |
|
f.解决方案:
a) 改用 fork 避免阻塞 beam.smp,erl -env ERL_NO_VFORK 1 ;
b) 减少文件句柄,如果确实需要大量的 open_port ,让另外一个专注的节点来做。
13.《 Erlang集群RPC通道拥塞问题及解决方案 》
知识点:
a.erlang 的消息发送是透明的,只要调用 Pid!Msg ,虚拟机和集群的基础设施会保证消息到达指定的进程的消息队列,这个是语义方面的保证。那么如果该 Pid 是在别的节点,这个消息就会通过节点间的 rpc 通道来传递。
b.目前社区推比较推荐 erlang 服务分层,所以层和层之间的交互基本上透过 rpc 来进行的。分层结构越来越多,当大量的消息在节点间流动的话,势必会造成通道拥塞。阻塞会导致发送进程被挂起,而 rpc 是单进程(gen_server)的,被挂起,rpc 调用就废了。当然除了 RPC,Pid!Msg 这种方式还是可以并行的走的。
c.当我们收到 {monitor, SusPid, busy_dist_port, Port} 消息的时候,就可以确认系统经常有阻塞问题(例如在 riak_sysmon 中的使用)。
d.可以通过修改 dist_buf_busy_limit 的值来解决。默认情况下,其值是 1M,一般情况下是够用了,但如果你的 rpc 没设计好,常常会返回大量的数据,这个值就可能不够了。
1 2 3 |
|
1 2 |
|
14.《 Erlang新添加选项 +zerts_de_busy_limit 控制节点间通讯的数据量 》
知识点:
a.erlang 节点间通信默认是通过 tcp 通道进行的, 而且每对节点间只有一个 tcp 链接,所有的 rpc 和内置的类似 monitor 这样的消息也都是通过这个通道进行的。当数据量过大的时候,系统就会发出 busy distribution port 警告,同时限制数据的吞吐。这个值默认是 128k。
b.可以通过 erl +zerts_de_busy_limit size 来修改这个值。
Set the value of erts_de_busy_limit. Larger values can help prevent busy distribution port system messages. The default limit is 128 kilobytes.
c.如果在 system monitor 的时候发现 busy dist port,不妨改大这个值,这个值的下限是 4k。
15.《 TCP链接主动关闭不发fin包奇怪行为分析 》
知识点:
a.发现一条 tcp 链接在 close 的时候,对端会收到 econnrest,而不是正常的 fin 包。通过抓包发现 close 系统调用的时候,我端发出 rst 报文,而不是正常的 fin。
b.在 net/ipv4/tcp.c:1900 附近,代码里面写的很清楚,如果你的接收缓冲区还有数据,协议栈就会发 rst 代替 fin 。
16.《 gen_tcp的close与delay_send交叉问题 》
知识点:
a.若干 tcp port 上有大量数据发送时,关闭这些 port,造成 erlang:ports/0 与 erlang:port_info/1 得到的 port 状态不一致。
b.erlang:ports/0 这个函数将当前系统中处于非 ERTS_PORT_SFLGS_DEAD 状态的 port 快照组成一个列表返回,得到当前所有的活动 port,这里有如下几点值得注意:a) ERTS_PORT_SFLGS_DEAD 状态由下列状态组成:ERTS_PORT_SFLG_FREE|ERTS_PORT_SFLG_FREE_SCHEDULED| ERTS_PORT_SFLG_INITIALIZING;b) 若在调用 erlang:ports/0 进行统计的期间内,某个 port 退出,则这个 port 也将位于 erlang:ports/0 的返回列表内,但这些 port 在下次调用 erlang:ports/0 时将不会再次出现。
c.erlang:port_info/1 这个函数仅将当前系统中处于非 ERTS_PORT_SFLGS_INVALID_LOOKUP 状态的 port 的 port_info 返回,这里有如下几点值得注意: a) ERTS_PORT_SFLGS_DEAD 状态由下列状态组成:ERTS_PORT_SFLG_FREE | ERTS_PORT_SFLG_FREE_SCHEDULED | ERTS_PORT_SFLG_INITIALIZING | ERTS_PORT_SFLG_INVALID | ERTS_PORT_SFLG_CLOSING;b) 与 erlang:ports/0 的区别在于,除了前三个状态,erlang:port_info/1 也不会将处于 ERTS_PORT_SFLG_INVALID 或 ERTS_PORT_SFLG_CLOSING 状态的 port_info 返回,其中 ERTS_PORT_SFLG_INVALID 不会真正赋予 port,而 ERTS_PORT_SFLG_CLOSING 可以被赋予 port。
d.ERTS_PORT_SFLG_CLOSING 状态是当前这个场景的问题核心,若一个 port 处于 ERTS_PORT_SFLG_CLOSING 状态,而不处于 ERTS_PORT_SFLGS_DEAD | ERTS_PORT_SFLG_FREE_SCHEDULED | ERTS_PORT_SFLG_INITIALIZING 状态,则它将出现在 erlang:ports/0 的列表中,同时在 erlang:port_info/1 的结果中返回 undefined。
e.作者设计场景(看原文)重现该问题,结果发现,server 的 port 在 delay_send 选项控制下,若发送缓冲有数据却强行 close,此时若 client 不 close 而仅仅是 shutdown,则 server 的 port 将不能释放,并出现 erlang:ports/0 与 erlang:port_info/1 不一致的情形,解决的办法是 server 也进行 shutdown,并令控制进程退出或 client 进行 close。
f.梳理了 gen_tcp:close/1 的执行流程:
a) unlink 掉 port 与当前进程的关系,使得 port 成为无主 port;
b) 将调用进程作为 port 的一个订阅进程,通过 subscribe 函数订阅 port 的 empty_out_q 消息,这个消息仅在 port 的发送缓冲被清空时,由虚拟机投递给订阅进程(也即当前的调用进程);
c) 循环等待 empty_out_q 消息的到达,或者触发 5 秒超时。若 empty_out_q 消息到达,则正常关闭 port 即可,若超时,则进入超时处理流程;
d) 超时后,通过 getstat(S, [send_pend]) 检查 port 的发送缓冲是否出现了变化,若未变化,则表明 port 的发送缓冲在过去的 5 秒内没能将任何数据发送出去,因此推测将来也不可能再将数据发送出去,因此强行关闭 port,若出现了变化,则继续循环等待,直到 port 的发送缓冲清空;
e) 强行关闭 port 将导致调用前述的 erts_do_exit_port 函数,在 port 的发送缓冲未清空的场景下,这个函数将设置 port 的状态为 ERTS_PORT_SFLG_CLOSING,导致调用 erlang:ports/0 与 erlang:port_info/1 观察到了 port 的中间状态,得到不一致的结果;
f) 这个问题不是资源泄露,而是发送缓冲这种设计机制导致的,port 在 close 前必须将发送缓冲的数据全部推送到客户端,而客户端如果既不 recv,也不 close,而是 shutdown 半关闭或不作为,则导致 server 的 port 仍然被占用,此时连接仍然存在,只是难于被观察到,通过 netstat 可以看到 client 套接字处于 FIN_WAIT2,而 server 套接字处于 CLOSING,符合半关闭的状态。
17.《 R15B01版本controlling_process一个port到self的问题 》
(略)
18.《 异步gen_server进行port访问时性能严重下降的原因和应对方法(五)套接字发送的应对 》
知识点:
a.从大道理上来讲,需要开发者预估一个进程的处理能力,不要向进程投递过多的消息以致于处理不完,如果处理不完,则需要重新设计,将消息分布到多个进程中处理;这确实是一个使用广泛的大道理,能解决一切,却好像又什么都没有解决,开发者需要不断摸索才能做到,我还在摸索中,所以就再次略过;
b.将异步接收消息的进程与调用 port(receive_match)的模式的进程分开;前文已经介绍过思路和优缺点,此处也就不再赘述;
c.拆分向 port 投递命令的过程,由进程来接收 port 回传的结果,而不是由模块接收;rabbitmq 已经给出了实现范例;
d.不使用 port 编写的模块,利用 nif 重新实现一套;目前还没有找到实现,但是由于网络连接随时可以断开,进程也必须收到连接断开的异步通知,因此必须要使用消息机制异步通知进程,因此完全通过 nif 实现套接字访问是不可接受的;
e.其它。
19.《 节点间通讯的通道微调 》
port 信息获取方法:
1 2 3 |
|
有了 Port,就可以设置 tcp port 的水位线,buffer 等等。
1 |
|
另外要注意 nodeup nodedown 可能会换了个 tcp 链接 要注意重新获取。
还有另外一种方法,设置所有 gen_tcp 的行为, 比如以下方法:
1 |
|
但是这个影响面非常大, 影响到正常 tcp 的参数了。
20.《 inet驱动新增加{active,N} socket选项 》
知识点:
a.效率最高的当然是 {active, true} 方式,因为这种实现相当于对于每一个连接,只执行一次 epoll_ctl 把 socket 的读事件挂到 epoll 上去的动作。
b.对于 {active,once} 方式,每次进行设定都意味着调用一次 epoll_ctl 。而 erlang 在实现中只使用一个线程来收割 epoll_wait 事件,如果大量的 epoll_ctl 动作阻塞了事件的收割,网络处理的能力会大大下降。
c.{active, true} 有安全问题,{active, once} 太慢, {active, N} 让我们一次设定接收 N 个消息包,摊薄 epoll_ctl 的代价,这样就可以大大缓解性能的压力。
d.霸爷说可以查看 lib/kernel/test/gen_tcp_misc_SUITE.erl 中的用法,但是没有找到使用 {active, N} 的地方。