Linux下TCP延迟确认(Delay ACK)机制

本文的起因是周师傅早上突然问起为何本应四次挥手的TCP,抓包发现只有三个包,看图显示为客户端主动发送FIN的情况下,少了一个服务器对客户端的ACK回复,而是直接发送了FIN+ACK。

此时我还不知道是因为Linux的Delay ACK机制造成的。于是针对这个现象展开了一波研究。

首先找到定义TCP协议的RFC793的文档 https://tools.ietf.org/html/rfc793 的3.5部分,文档表示

     TCP A                                                TCP B

  1.  ESTABLISHED                                          ESTABLISHED

  2.  (Close)
      FIN-WAIT-1  --> <SEQ=100><ACK=300><CTL=FIN,ACK>  --> CLOSE-WAIT

  3.  FIN-WAIT-2  <-- <SEQ=300><ACK=101><CTL=ACK>      <-- CLOSE-WAIT

  4.                                                       (Close)
      TIME-WAIT   <-- <SEQ=300><ACK=101><CTL=FIN,ACK>  <-- LAST-ACK

  5.  TIME-WAIT   --> <SEQ=101><ACK=301><CTL=ACK>      --> CLOSED

  6.  (2 MSL)
      CLOSED

                         Normal Close Sequence

                               Figure 13.



      TCP A                                                TCP B

  1.  ESTABLISHED                                          ESTABLISHED

  2.  (Close)                                              (Close)
      FIN-WAIT-1  --> <SEQ=100><ACK=300><CTL=FIN,ACK>  ... FIN-WAIT-1
                  <-- <SEQ=300><ACK=100><CTL=FIN,ACK>  <--
                  ... <SEQ=100><ACK=300><CTL=FIN,ACK>  -->

  3.  CLOSING     --> <SEQ=101><ACK=301><CTL=ACK>      ... CLOSING
                  <-- <SEQ=301><ACK=101><CTL=ACK>      <--
                  ... <SEQ=101><ACK=301><CTL=ACK>      -->

  4.  TIME-WAIT                                            TIME-WAIT
      (2 MSL)                                              (2 MSL)
      CLOSED                                               CLOSED

                      Simultaneous Close Sequence

显然不管是单方发起还是双方同时发起关闭连接,都是有四次挥手的。

只好从源代码下手去寻找答案。由于周师傅抓包的是HTTP的情况,于是从服务器端的nginx源码研究起。
nginx/src/os/unix/ngx_socket.h里显示

ioctl(FIONBIO) sets a non-blocking mode with the single syscall
while fcntl(F_SETFL, O_NONBLOCK) needs to learn the current state
using fcntl(F_GETFL).
ioctl() and fcntl() are syscalls at least in FreeBSD 2.x, Linux 2.2
and Solaris 7.
ioctl() in Linux 2.4 and 2.6 uses BKL, however, fcntl(F_SETFL) uses it too.

nginx是使用系统函数来管理TCP连接的。所以这不是nginx的锅,于是找到Linux内核源码来研究系统对TCP的管理。

首先找到linux/net/ipv4/tcp.c来观察系统对于TCP_ESTABLISHED时收到FIN包如何切换状态。
在tcp_set_state函数中找到了linux对tcp状态的变化维护表

static const unsigned char new_state[16] = {
  /* current state:        new state:      action:    */
  [0 /* (Invalid) */]	= TCP_CLOSE,
  [TCP_ESTABLISHED]	= TCP_FIN_WAIT1 | TCP_ACTION_FIN,
  [TCP_SYN_SENT]	= TCP_CLOSE,
  [TCP_SYN_RECV]	= TCP_FIN_WAIT1 | TCP_ACTION_FIN,
  [TCP_FIN_WAIT1]	= TCP_FIN_WAIT1,
  [TCP_FIN_WAIT2]	= TCP_FIN_WAIT2,
  [TCP_TIME_WAIT]	= TCP_CLOSE,
  [TCP_CLOSE]		= TCP_CLOSE,
  [TCP_CLOSE_WAIT]	= TCP_LAST_ACK  | TCP_ACTION_FIN,
  [TCP_LAST_ACK]	= TCP_LAST_ACK,
  [TCP_LISTEN]		= TCP_CLOSE,
  [TCP_CLOSING]		= TCP_CLOSING,
  [TCP_NEW_SYN_RECV]	= TCP_CLOSE,	/* should not happen ! */
};

而对FIN包的处理是由linux/net/ipv4/tcp_input.c里面的tcp_fin函数负责

/*
 * 	Process the FIN bit. This now behaves as it is supposed to work
 *	and the FIN takes effect when it is validly part of sequence
 *	space. Not before when we get holes.
 *
 *	If we are ESTABLISHED, a received fin moves us to CLOSE-WAIT
 *	(and thence onto LAST-ACK and finally, CLOSE, we never enter
 *	TIME-WAIT)
 *
 *	If we are in FINWAIT-1, a received FIN indicates simultaneous
 *	close and we go into CLOSING (and later onto TIME-WAIT)
 *
 *	If we are in FINWAIT-2, a received FIN moves us to TIME-WAIT.
 */


void tcp_fin(struct sock *sk)
{
	struct tcp_sock *tp = tcp_sk(sk);

	inet_csk_schedule_ack(sk);

	sk->sk_shutdown |= RCV_SHUTDOWN;
	sock_set_flag(sk, SOCK_DONE);

	switch (sk->sk_state) {
	case TCP_SYN_RECV:
	case TCP_ESTABLISHED:
		/* Move to CLOSE_WAIT */
		tcp_set_state(sk, TCP_CLOSE_WAIT);
		inet_csk(sk)->icsk_ack.pingpong = 1;
		break;

	case TCP_CLOSE_WAIT:
	case TCP_CLOSING:
		/* Received a retransmission of the FIN, do
		 * nothing.
		 */
		break;
	case TCP_LAST_ACK:
		/* RFC793: Remain in the LAST-ACK state. */
		break;

	case TCP_FIN_WAIT1:
		/* This case occurs when a simultaneous close
		 * happens, we must ack the received FIN and
		 * enter the CLOSING state.
		 */
		tcp_send_ack(sk);
		tcp_set_state(sk, TCP_CLOSING);
		break;
	case TCP_FIN_WAIT2:
		/* Received a FIN -- send ACK and enter TIME_WAIT. */
		tcp_send_ack(sk);
		tcp_time_wait(sk, TCP_TIME_WAIT, 0);
		break;
	default:
		/* Only TCP_LISTEN and TCP_CLOSE are left, in these
		 * cases we should never reach this piece of code.
		 */
		pr_err("%s: Impossible, sk->sk_state=%d\n",
		       __func__, sk->sk_state);
		break;
	}

	/* It _is_ possible, that we have something out-of-order _after_ FIN.
	 * Probably, we should reset in this case. For now drop them.
	 */
	skb_rbtree_purge(&tp->out_of_order_queue);
	if (tcp_is_sack(tp))
		tcp_sack_reset(&tp->rx_opt);
	sk_mem_reclaim(sk);

	if (!sock_flag(sk, SOCK_DEAD)) {
		sk->sk_state_change(sk);

		/* Do not send POLL_HUP for half duplex close. */
		if (sk->sk_shutdown == SHUTDOWN_MASK ||
		    sk->sk_state == TCP_CLOSE)
			sk_wake_async(sk, SOCK_WAKE_WAITD, POLL_HUP);
		else
			sk_wake_async(sk, SOCK_WAKE_WAITD, POLL_IN);
	}
}

函数做了状态转换和乱序包的处理。而在TCP_ESTABLISHED这里发现一个有趣的现象,没有马上调用tcp_send_ack进行ACK回复,而是执行了
inet_csk(sk)->icsk_ack.pingpong = 1;
这个icsk_ack.pingpong是什么呢,顺藤摸瓜的找到了include/net/inet_connection_sock.h

@icsk_ack: Delayed ACK control data

	struct {
		__u8		  pending;	 /* ACK is pending			   */
		__u8		  quick;	 /* Scheduled number of quick acks	   */
		__u8		  pingpong;	 /* The session is interactive		   */
		__u8		  blocked;	 /* Delayed ACK was blocked by socket lock */
		__u32		  ato;		 /* Predicted tick of soft clock	   */
		unsigned long	  timeout;	 /* Currently scheduled timeout		   */
		__u32		  lrcvtime;	 /* timestamp of last received data packet */
		__u16		  last_seg_size; /* Size of last incoming segment	   */
		__u16		  rcv_mss;	 /* MSS used for delayed ACK decisions	   */ 

其中pingpong的作用是表明这个session是交互式。这个标志位是delay ack的一个control位。查阅了一下icsk_ack.pingpong和delay ack的资料,发现正常的挥手流程是这样的

  1. client: FIN (will not send more)
  2. server: ACK (received the FIN)
    … server: sends more data…, client ACKs these data
  3. server: FIN (will not send more)
  4. client: ACK (received the FIN)

而下面有说

If the server has no more data to send it might close the connection also. In this case steps 2+3 can be merged, e.g. the server sends a FIN+ACK, where the ACK acknowledges the FIN received by the client.

也就是说如果服务器端在客户端发出FIN以后,如果有数据要发送,需要先ACK这个FIN,然后再进行数据发送。但是如果服务器端没有更多数据发送,也要关闭连接的情况下,很可能ACK包就跟随FIN一起发出。其中ACK为确认客户端的FIN包。

查询RFC1122的 4.2.3.2 When to Send an ACK Segment 得知

         4.2.3.2  When to Send an ACK Segment
            A host that is receiving a stream of TCP data segments can
            increase efficiency in both the Internet and the hosts by
            sending fewer than one ACK (acknowledgment) segment per data
            segment received; this is known as a "delayed ACK" [TCP:5].

            A TCP SHOULD implement a delayed ACK, but an ACK should not
            be excessively delayed; in particular, the delay MUST be
            less than 0.5 seconds, and in a stream of full-sized
            segments there SHOULD be an ACK for at least every second
            segment.

原来:
TCP采用两种方式来发送ACK:快速确认和延迟确认。
在快速确认模式中,本端接收到数据包后,会立即发送ACK给对端。
在延迟确认模式中,本端接收到数据包后,不会立即发送ACK给对端,而是等待一段时间,如果在此期间:

  1. 本端有数据包要发送给对端。就在发送数据包的时候捎带上此ACK,如此一来就节省了一个报文。
  2. 本端没有数据包要发送给对端。延迟确认定时器会超时,然后发送纯ACK给对端。

具体实现上面用
icsk->icsk_ack.pingpong == 0,表示使用快速确认。
icsk->icsk_ack.pingpong == 1,表示使用延迟确认。
而对周师傅遇到的情况的FIN包的处理刚好是在icsk->icsk_ack.pingpong == 1的场景。于是服务端的FIN和ACK合并发送了。

参考资料:
https://github.com/torvalds/linux/
http://blog.csdn.net/wdscq1234/article/details/52430382
http://blog.csdn.net/dog250/article/details/52664508
http://stackoverflow.com/questions/21390479/fin-omitted-fin-ack-sent

关于进化

以下为个人理解和读书笔记

  • 生物进化在尺寸、速度和能源消耗方面有计算机模拟无可比拟的优势,条件适合,一小时内可以产生出十亿个副本。

  • 核糖核酸统一了信息和机体,既是表现形式,又是内在成因。既要充当信使,本身又是信息。既要担当起与世界互动的责任,又要完成延续世界的重任,把信息传递给下一代。本身又极为紧凑。人工进化正可以由此展开。

  • 定向进化是另一种监督式学习,选择由培育者引导。

  • 对个体而言最好的,对物种而言却不一定。

  • 进化是一种计算。

  • 弱学习

  • 生物DNA无法将自己的代码向其他生物体“广而告之”。

  • 达尔文系统缺陷在于无法把已获得的有利的知识和变化引入到遗传和进化中。只能通过死亡等消除不利变化。

  • 非达尔文进化系统,拉马克进化-获得性遗传,有用的变异能更快的进入基因序列。

  • 拉马克系统缺陷在于对于一个有利的变化,需要回溯基因构成,像质因数分解一样难度极高。是一种不可能存在的生物解密方案。但是在计算机进化中可以通过“表里如一”的自复制实现。(地球上的生命已经通过了自复制分子这个阶段?与系统复杂性相关?)

  • 拉马克系统允许个体在世时所获的信息可以参与进化。

  • 蚁群算法,单只蚂蚁毕生学习所得成为蚁群信息遗产的一部分。

  • 投入“传播”的信息量非常少,范围非常小,信号非常弱。弱传播。

  • 人类的学习(知识文化传递)就是一种对没有拉马克进化-获得性遗传的弥补?

  • 并行系统是水平的、并发的、错综复杂的因果网络,非线性特征,没有清晰的步骤可循,事件此起彼伏。为其编程很困难。

  • 电网,电话网,计算机网络,金融网络都是并行系统。

  • 人类个体无法掌握充分利用并行处理能力的方法。超出我们的掌控能力。

  • 人类应该只编写那些小而精,快而准的个体程序,注入自然进化来进行并行。

  • 程序过于庞大无法完整测试和保证没有缺陷。而进化出来的东西在成长环境里有足够多的测试。

  • 程序过于庞大,仅仅维护程序、保持正常运行本身将会成为一个主要负担。(程序员24小时待命)

  • 人工进化可以进化出更完美的东西,进化能看护我们无法看护的世界。

  • 进化的代价就是失控。我们放弃了某些控制。

  • 与其正确,不如灵活,不如耐久。蚂蚁对身处的世界茫然无知。

  • 舍控制取力量。

技术面试指南

面试流程

通常我们的面试分为一次电话面试和一次现场面试。在少数难以决定的时候会多增加一轮电话或现场面试。

面试中的沟通问题

尊重候选人,平等交流:让候选人自我介绍前,先介绍自己和公司;交流的时候双方处于平等的地位,耐心听完对方的话;在面试过程中不要有驳倒候选人的意图。无论是电话面试还是现场面试都应该做到守时。我们在考察别人的时候,别人也在考察我们。

把握好面试节奏:面试从双方的自我介绍开始,然后开始从候选人过去的职位和做过的项目开始谈起,这些都应该是候选人熟悉的内容,为后面更难的技术问题建立良好的沟通氛围。

不要只求答案正确,要本着一起讨论的方式让候选人充分说明解题思路;不要不断地变换问题,每个问题点到即止。对一个问题要一层层深入,直到候选人回答不了或完整解答为止,这样才能知道候选人思考达到的深度在哪里。

面试的重点是考察候选人解决办法的思路。可以从一个简单的问题开始,候选人给出回答后,在上一个问题基础上做些变化进一步加大难度,考察候选人思路是否灵活;也可以从一个困难的问题开始,考察候选人分解复杂问题的能力,在长时间没有进展时应该给出一些提示。同时也要注意考察候选人在遇到困难时是否会问合适的问题。

给候选人对你提问的机会:我们要通过面试了解候选人,候选人也需要在这个过程中了解我们。在面试结束前应该给候选人提出对我们的团队、产品、面试过程、职位需求等方面问题的机会。一来解答对方的疑问,二来也可以看出他对新工作的期待程度和热情高低。

每一次面试,并不仅仅是一次壮大团队的机会。哪怕最后没有招来新同事,也可以多让一个人知道我们的公司和产品。我们的用户群和我们招聘的目标人群是重叠的,我们接触的每个人都可能传播我们的形象和品牌。

面试中需要考察的问题

对不同的技术职位下面的几个方面有不同的权重,但都应该基本覆盖到:

  • 基础知识:基本的数据结构和算法;
  • 排序、二分查找等经典算法在现实中的应用;
  • 对概率的基本理解;
  • 对时间和空间复杂度的理解;
  • 所招聘职位相关的专业问题(iOS、Android、后端架构等)。

面试中应避免的问题

  • 与技术和实际工作无关的智力题,比如过去曾很流行的和海盗、金币、球相关的一类问题。因为在面试中能比较快回答出这类问题的通常是曾在网上看到过答案的人,并且这类问题对预测候选人在实际工作中的表现几乎没有参考价值。
  • 网上常见的所谓 Microsoft、Google 面试题等,原因同上。
  • 非技术的假设性问题:假如现在要赶活;假如有两件事情摆在你面前;假如老板故意刁难你;假如有一个不好合作的同事。

转自:http://open.leancloud.cn/tech-interview-guide.html

代码分支管理指南

介绍

这是我们团队的 Git 分支管理规范。每个人对工具的使用往往各有偏好,各种方法各有利弊,无所谓对错。但涉及团队协作的方面需要有一些一致的规范,所以请大家务必遵守。

除了一致性之外,这个规范的目的是以下几点:

  • 确保可以轻易确定特定时间发布或运行的版本。在新发布的程序存在重大缺陷时,可以尽快 rollback 到上一个稳定版本。
  • 在需要修复紧急 bug 并尽快发布时,可以只发布必要的 bugfix 而不同时发布还不应发布的其他改动。

branch 和 tag

每个官方的 repo(leancloud/ 下的都是官方 repo)有且仅有以下的 branch 和 tag。

Branch: master release。其中 master 对应目前的开发分支,所有的 pull request 都应该发到这个分支。release 是当前发布的分支,在这个分支只能增加从 master cherrypick 过来的 commit。详见本文后面的说明。

Tag: 对应每个发布版本的 tag。SDK 和应用程序的 tag 遵照 <major>.<minor>.<patch> 的命名,如 2.5.1;服务端程序的 tag 以发布的日期命名,如 2014.11.13,如果有 bugfix,则在后面增加小写字母,如 2014.11.13 后是 2014.11.13a,然后是 2014.11.13b

目前还有部分 repo 包含多个独立部署的项目(如 uluru-platform)。在这样的 repo 打 tag 时需要附上项目名做前缀,如 bigquery-2.5.1。但我们需要逐步把这些项目拆分到独立的 repo。

发布新版流程

  • 确保所有要发布的 pull request 都已经 merge 到 master
  • 使用 master branch 的代码进行测试,如果发现 bug,把对应的 bugfix merge 到 master
  • 删除旧的 release branch,并从当前的 master 创建新的 release branch;
  • 在 Jenkins 上从releasebranch 发起新的 build 并发布;
  • 发布完成后在当前的 release branch 打上对应版本的 tag。

Bugfix 流程

这里的 bugfix 指的是修复已经发布的程序(release branch)中的缺陷。master 里的 bug 请直接 merge bugfix 到 master

  • 如果此缺陷在master中还存在,请先 merge bugfix 到master ,否则跳到下一步;
  • releasebranch 从mastercherrypick 修复该缺陷的一个或多个 commit;
  • 在 Jenkins 上发布当前releasebranch;
  • 发布完成后在当前的releasebranch 打上递增的 tag。比如,如果上一个 tag 是2.5.1 ,这个 tag 应该是2.5.2 ;如果上一个是2014.11.13 ,这个就是2014.11.13a

其他

并不是每个 bug 都有专门发布 bugfix 版的必要,对于不紧急的 bug,可以在master里 fix 后随下一个版本发布。

在一个官方 repo 下只应该有以上说的 branch 和 tag,在开发过程中使用到的 feature branch 等请都放在个人的 fork,一律通过向master发 pull request 的方式给官方 repo 提交代码。

转自http://open.leancloud.cn/git-branch-guide.html

工作的评价和反馈机制

评价和反馈

我们在每个季度结束时会进行一次 performance review,即工作的评价和反馈。流程一定程度上借鉴了 Google 的 performance review, 但有不少简化和修改,以避免给大家造成额外的负担,毕竟我们的主要精力应该放在改进产品而不是处理内部流程上。

这个流程分为两部分。

自我评价及工作反馈

在季度结束时,每个同事会收到一个 Google Docs 表格,包含以下几个问题。除了第一个问题外,其他内容都会对其他同事保密,只有自己的主管和 HR 可以看到。

自我评价

  • 请列出过去一个季度你参与的工作、承担的职责、完成的具体内容,并陈述工作实际产生的价值。请尽可能详尽。如果有在自- 己日常职责之外的贡献,也请单独列出。(这部分内容将对所有人公开)
  • 针对以上列出的工作请给出对自己工作的评价。请总结得失以及原因。有哪些地方有改进的空间?
  • 针对上面的问题和需要做的改进,请列出在下个季度的具体改进计划。

工作反馈

  • 公司在哪些方面给你提供更多资源或支持可以让你工作得更好?
  • 对于你的主管或管理团队的工作有哪些反馈和建议?
  • 对于团队建设、公司文化有哪些反馈和建议?

主管评价

每个担任 people manager 的同事会收到下属的自我评价和工作反馈。每个 manager 会为每位下属写主管评价和反馈,同时打出本季度的绩效得分。绩效分数在 0.0 至 2.0 之间,其中 1.0 表示工作达到期望,低于 1.0 表示低于期望,高于 1.0 表示高于期望。这里的「期望」和每个人的职能、级别和薪酬相关。

对薪酬的影响

绩效分数对薪酬的影响体现在年终奖上。我们的年终奖计算公式为:

年终奖 = 本年度累计实际工资 * 15% * 年度个人绩效 * 年度公司绩效

其中的年度个人绩效即为个人各季度绩效分数的平均数。年度公司绩效由管理团队在年末评定。

结语

我们的 performance review 首要目的是为每个人提供一个总结工作并听取反馈,明确得失以便改进的机会;次要目的是通过浮动的年终奖让做出更多贡献的同事能得到更高回报,做到相对的公平。希望每个人都以坦诚、认真、实事求是的态度对待这项工作。

转自http://open.leancloud.cn/perf-review.html