Jack Jiang

我的最新工程MobileIMSDK:http://git.oschina.net/jackjiang/MobileIMSDK
posts - 219, comments - 13, trackbacks - 0, articles - 0

置顶随笔

     摘要: 本文的上篇我们讨论了在线实时消息的投递,如果接收方用户B不在线,系统是如何保证离线消息的可达性的呢?这就是本文要讨论的问题。  阅读全文

posted @ 2016-11-18 14:39 Jack Jiang 阅读(2864) | 评论 (0)编辑 收藏

     摘要: 虽然C10K问题已被妥善解决,但对于即时通讯应用(或其它网络编程方面)的开发者而言,研究C10K问题仍然价值巨大,因为技术的发展都是有规律和线索可循的,了解C10K问题及其解决思路,通过举一反三,或许可以为你以后面对类似问题提供更多可借鉴的思想和解决问题的实践思路。而这,也正是撰写本文的目的所在。  阅读全文

posted @ 2016-10-21 16:02 Jack Jiang 阅读(2485) | 评论 (0)编辑 收藏

     摘要: 本文将以新手的视角引导你阅读相关文章,以便为从零开发一个移动端IM做好方方面面的知识准备:包括但不限于网络编程基础、通信协议的选型、IM的架构设计等等。  阅读全文

posted @ 2016-08-29 17:42 Jack Jiang 阅读(3012) | 评论 (0)编辑 收藏

     摘要: 本文将简要介绍TeamTalk开源的过去和现在,为打算研究和采用TeamTalk的同行提供一定程度的参考。  阅读全文

posted @ 2016-08-09 17:25 Jack Jiang 阅读(2594) | 评论 (0)编辑 收藏

     摘要: 本文基于作者的实践以及相关资料的整理,总结了自已对Android进程和Service保活的理解,希望能为你的应用开发带来启发。  阅读全文

posted @ 2016-08-02 22:43 Jack Jiang 阅读(2370) | 评论 (0)编辑 收藏

     摘要: 本文将介绍如何在现有的技术基础上选择合适的方案开发一个“服务器推”(Comet技术)的应用,最优的方案还是取决于应用需求的本身。相对于传统的 Web 应用, 开发 Comet 应用具有一定的挑战性。  阅读全文

posted @ 2016-07-28 11:07 Jack Jiang 阅读(1377) | 评论 (0)编辑 收藏

     摘要: 本文对服务器推送技术(SSE)进行了详细的介绍,包含浏览器端和服务器端的相应实现细节,为在实践中使用该技术提供了指南  阅读全文

posted @ 2016-07-22 18:03 Jack Jiang 阅读(1041) | 评论 (0)编辑 收藏

     摘要: Web端即时通讯技术因受限于浏览器的设计限制,一直以来实现起来并不容易,主流的Web端即时通讯方案大致有4种:传统Ajax短轮询、Comet技术、WebSocket技术、SSE(Server-sent Events)。本文将简要介绍这4种技术的原理,并指出各自的异同点、优缺点等。  阅读全文

posted @ 2016-07-15 15:08 Jack Jiang 阅读(1617) | 评论 (2)编辑 收藏

     摘要: Web端的IM应用,由于浏览器的兼容性以及其固有的“客户端请求服务器处理并响应”的通信模型,造成了要在浏览器中实现一个兼容性较好的IM应用,其通信过程必然是诸多技术的组合,本文的目的就是要详细探讨这些技术并分析其原理和过程。   阅读全文

posted @ 2016-07-12 15:59 Jack Jiang 阅读(5301) | 评论 (0)编辑 收藏

     摘要: 文演示的是一个Android客户端程序,通过UDP协议与两个典型的NIO框架服务端(分别用MINA2和Netty4来实现),实现跨平台双向通信的完整Demo。  阅读全文

posted @ 2016-06-30 16:57 Jack Jiang 阅读(614) | 评论 (0)编辑 收藏

     摘要: 本文将演示一个iOS客户端程序,通过UDP协议与两个典型的NIO框架服务端(将分别用MINA2和Netty4来实现),实现跨平台双向通信的完整Demo。  阅读全文

posted @ 2016-06-28 22:11 Jack Jiang 阅读(1218) | 评论 (0)编辑 收藏

     摘要: 本文是《NIO框架入门》系列文章中的第2篇,将演示的是一个基于MINA2的UDP服务端和一个标准UDP客户端(Java实现)双向通信的完整例子。  阅读全文

posted @ 2016-06-24 14:38 Jack Jiang 阅读(639) | 评论 (0)编辑 收藏

     摘要: 本文将演示的是一个基于Netty4的UDP服务端和一个标准UDP客户端(Java实现)双向通信的完整例子。实际上,Netty4的UDP例子非常难找,官方的代码演示里只有一个简单的UDP广播例子,不足以用于演示Netty4的UDP通信最佳实践。  阅读全文

posted @ 2016-06-20 14:48 Jack Jiang 阅读(1201) | 评论 (0)编辑 收藏

     摘要: MobileIMSDK是一套专为移动端开发的原创即时通讯框架:超轻量级、高度提炼,lib包50KB以内;完全基于UDP协议实现;客户端支持iOS、Android、标准Java平台;可应用于跨设备、跨网络的聊天APP、企业OA、消息推送等各种场景。  阅读全文

posted @ 2015-12-14 15:18 Jack Jiang 阅读(2642) | 评论 (0)编辑 收藏

     摘要: MobileIMSDK是专为移动端开发的原创即时通讯开源框架:超轻量级、高度提炼,lib包50KB以内;完全基于UDP协议实现;客户端支持iOS、Android、标准Java平台;可应用于跨设备、跨网络的聊天APP、企业OA、消息推送等各种场景。  阅读全文

posted @ 2015-12-01 16:06 Jack Jiang 阅读(3149) | 评论 (2)编辑 收藏

2021年2月24日

本文原题“你管这破玩意儿叫TCP?”,由闪客sun分享,转载请联系作者。

1、引言

网络编程能力对于即时通讯技术开发者来说是基本功,而计算机网络又是网络编程的理论根基,因而深刻准确地理解计算机网络知识显然能夯实你的即时通讯应用的实践品质。

本文风格类似于《网络编程懒人入门》、《脑残式网络编程入门》两个系列,但通俗又不失内涵,简洁又不简陋,非常适合对计算机网络知识有向往但又有惧怕的网络编程爱好者们阅读,希望能给你带来不一样的网络知识入门视角。

本篇将运用通俗易懂的语言,配上细致精确的图片动画,循序渐进地引导你理解TCP协议的主要特性和技术原理,让TCP协议的学习不再如此枯燥和生涩,非常适合入门者阅读。

本文已同步发布于“即时通讯技术圈”公众号,欢迎关注。公众号上的链接是:点此进入

2、系列文章

本文是该系列文章中的第2篇:

本文主要涉及计算机网络的传输层,希望让TCP协议的学习不再枯燥和生涩。

3、初识传输层

你是一台电脑,你的名字叫 A。

网络编程入门从未如此简单(二):假如你来设计TCP协议,会怎么做?_1-1.png
经过上篇《假如你来设计网络,会怎么做?》的一番折腾,只要你知道另一位伙伴 B 的 IP 地址,且你们之间的网络是通的,无论多远,你都可以将一个数据包发送给你的伙伴 B。

网络编程入门从未如此简单(二):假如你来设计TCP协议,会怎么做?_1-2.png

上篇中分享的这就是物理层、数据链路层、网络层这三层所做的事情。

站在第四层的你,就可以不要脸地利用下三层所做的铺垫,随心所欲地发送数据,而不必担心找不到对方了。

网络编程入门从未如此简单(二):假如你来设计TCP协议,会怎么做?_1-3.gif

虽然你此时还什么都没干,但你还是给自己这一层起了个响亮的名字,叫做传输层。

你本以为自己所在的第四层万事大吉,啥事没有,但很快问题就接踵而至。

4、问题来了

前三层协议只能把数据包从一个主机搬到另外一台主机,但是到了目的地以后,数据包具体交给哪个程序(进程)呢?

网络编程入门从未如此简单(二):假如你来设计TCP协议,会怎么做?_2-1.png

所以:你需要把通信的进程区分开来,于是就给每个进程分配一个数字编号,你给它起了一个响亮的名字:端口号。

网络编程入门从未如此简单(二):假如你来设计TCP协议,会怎么做?_2-2.png

然后:你在要发送的数据包上,增加了传输层的头部:源端口号与目标端口号。

网络编程入门从未如此简单(二):假如你来设计TCP协议,会怎么做?_2-3.png

OK,这样你将原本主机到主机的通信,升级为了进程和进程之间的通信。

你没有意识到,你不知不觉实现了UDP协议

当然 UDP 协议中不光有源端口和目标端口,还有数据包长度和校验值,我们暂且略过。

就这样,你用 UDP 协议无忧无虑地同 B 进行着通信,一直没发生什么问题。

网络编程入门从未如此简单(二):假如你来设计TCP协议,会怎么做?_2-4.gif

但很快,你发现事情变得非常复杂 ... ...

5、丢包问题

由于网络的不可靠,数据包可能在半路丢失,而 A 和 B 却无法察觉。

网络编程入门从未如此简单(二):假如你来设计TCP协议,会怎么做?_3-1.gif

对于丢包问题,只要解决两个事就好了。

第一个:A 怎么知道包丢了?
答案是:让 B 告诉 A。

第二个:丢了的包怎么办?
答案是:重传。

于是你设计了如下方案:A 每发一个包,都必须收到来自 B 的确认(ACK),再发下一个,否则在一定时间内没有收到确认,就重传这个包。

网络编程入门从未如此简单(二):假如你来设计TCP协议,会怎么做?_3-2.gif

你管它叫停止等待协议。

只要按照这个协议来,虽然 A 无法保证 B 一定能收到包,但 A 能够确认 B 是否收到了包,收不到就重试,尽最大努力让这个通信过程变得可靠,于是你们现在的通信过程又有了一个新的特征,可靠交付。

6、效率问题

停止等待虽然能解决问题,但是效率太低了。

A 原本可以在发完第一个数据包之后立刻开始发第二个数据包,但由于停止等待协议,A 必须等数据包到达了 B ,且 B 的 ACK 包又回到了 A,才可以继续发第二个数据包。这效率慢得可不是一点两点。

于是:你对这个过程进行了改进,采用流水线的方式,不再傻傻地等。

网络编程入门从未如此简单(二):假如你来设计TCP协议,会怎么做?_4-1.gif

7、顺序问题

但是网路是复杂的、不可靠的。

这导致的问题是:有的时候 A 发出去的数据包,分别走了不同的路由到达 B,可能无法保证和发送数据包时一样的顺序。

网络编程入门从未如此简单(二):假如你来设计TCP协议,会怎么做?_5-1.gif

对应于我们的例子:在流水线中有多个数据包和ACK包在乱序流动,他们之间对应关系就乱掉了。

如果回到上面的停止等待协议,那么A 每收到一个包的确认(ACK)再发下一个包,那就根本不存在顺序问题。但,应该有更好的办法吧?

是的,更好的办法就是:A 在发送的数据包中增加一个序号(seq),同时 B 要在 ACK 包上增加一个确认号(ack)。这样不但解决了停止等待协议的效率问题,也通过这样标序号的方式解决了顺序问题。

网络编程入门从未如此简单(二):假如你来设计TCP协议,会怎么做?_5-2.gif

而 B 这个确认号意味深长:比如 B 发了一个确认号为 ack = 3,它不仅仅表示 A 发送的序号为 2 的包收到了,还表示 2 之前的数据包都收到了。这种方式叫累计确认累计应答

网络编程入门从未如此简单(二):假如你来设计TCP协议,会怎么做?_5-3.gif

注意:实际上 ack 的号是收到的最后一个数据包的序号 seq + 1,也就是告诉对方下一个应该发的序号是多少。但图中为了便于理解,ack 就表示收到的那个序号,不必纠结。

8、流量问题

有的时候,A 发送数据包的速度太快,而 B 的接收能力不够,但 B 却没有告知 A 这个情况。

网络编程入门从未如此简单(二):假如你来设计TCP协议,会怎么做?_6-1.gif

怎么解决呢?

很简单:B 告诉 A 自己的接收能力,A 根据 B 的接收能力,相应控制自己的发送速率就好了。

B 怎么告诉 A 呢?B 跟 A 说"我很强"这三个字么?那肯定不行,得有一个严谨的规范。

于是 B 决定:每次发送数据包给 A 时,顺带传过来一个值,叫窗口大小(win),这个值就表示 B 的接收能力

同理:每次 A 给 B 发包时也带上自己的窗口大小,表示 A 的接收能力。

网络编程入门从未如此简单(二):假如你来设计TCP协议,会怎么做?_6-2.gif

B 告诉了 A 自己的窗口大小值,A 怎么利用它去做 A 这边发包的流量控制呢?

很简单:假如 B 给 A 传过来的窗口大小 win = 5,那 A 根据这个值,把自己要发送的数据分成这么几类。

网络编程入门从未如此简单(二):假如你来设计TCP协议,会怎么做?_6-3.png

图片过于清晰,就不再文字解释了。

当 A 不断发送数据包时,已发送的最后一个序号就往右移动,直到碰到了窗口的上边界,此时 A 就无法继续发包,达到了流量控制。

网络编程入门从未如此简单(二):假如你来设计TCP协议,会怎么做?_6-4.gif

但是:当 A 不断发包的同时,A 也会收到来自 B 的确认包,此时整个窗口会往右移动,因此上边界也往右移动,A 就能发更多的数据包了。

网络编程入门从未如此简单(二):假如你来设计TCP协议,会怎么做?_6-5.gif

以上都是在窗口大小不变的情况下。而 B 在发给 A 的 ACK 包中,每一个都可以重新设置一个新的窗口大小,如果 A 收到了一个新的窗口大小值,A 会随之调整。

如果 A 收到了比原窗口值更大的窗口大小,比如 win = 6,则 A 会直接将窗口上边界向右移动 1 个单位。

网络编程入门从未如此简单(二):假如你来设计TCP协议,会怎么做?_6-6.gif

如果 A 收到了比原窗口值小的窗口大小,比如 win = 4,则 A 暂时不会改变窗口大小,更不会将窗口上边界向左移动,而是等着 ACK 的到来,不断将左边界向右移动,直到窗口大小值收缩到新大小为止。

网络编程入门从未如此简单(二):假如你来设计TCP协议,会怎么做?_6-7.gif

OK,终于将流量控制问题解决得差不多了,你看着上面一个个小动图,给这个窗口起了一个更生动的名字:滑动窗口。

9、拥塞问题

但有的时候,不是 B 的接受能力不够,而是网络不太好,造成了网络拥塞。

网络编程入门从未如此简单(二):假如你来设计TCP协议,会怎么做?_7-1.gif

拥塞控制与流量控制有些像,但流量控制是受 B 的接收能力影响,而拥塞控制是受网络环境的影响。

拥塞控制的解决办法依然是通过设置一定的窗口大小。只不过,流量控制的窗口大小是 B 直接告诉 A 的,而拥塞控制的窗口大小按理说就应该是网络环境主动告诉 A。

但网络环境怎么可能主动告诉 A 呢?只能 A 单方面通过试探,不断感知网络环境的好坏,进而确定自己的拥塞窗口的大小。

网络编程入门从未如此简单(二):假如你来设计TCP协议,会怎么做?_7-2.gif

拥塞窗口大小的计算有很多复杂的算法,就不在本文中展开了(有兴趣可以深入阅读《[通俗易懂]深入理解TCP协议(下):RTT、滑动窗口、拥塞处理》)。

假如拥塞窗口的大小为  cwnd,上一部分流量控制的滑动窗口的大小为 rwnd,那么窗口的右边界受这两个值共同的影响,需要取它俩的最小值。

窗口大小 = min(cwnd, rwnd)

含义很容易理解:当 B 的接受能力比较差时,即使网络非常通畅,A 也需要根据 B 的接收能力限制自己的发送窗口。当网络环境比较差时,即使 B 有很强的接收能力,A 也要根据网络的拥塞情况来限制自己的发送窗口。正所谓受其短板的影响嘛~

10、连接问题

有的时候,B 主机的相应进程还没有准备好或是挂掉了,A 就开始发送数据包,导致了浪费。

网络编程入门从未如此简单(二):假如你来设计TCP协议,会怎么做?_8-1.gif

这个问题在于:A 在跟 B 通信之前,没有事先确认 B 是否已经准备好,就开始发了一连串的信息。就好比你和另一个人打电话,你还没有"喂"一下确认对方有没有在听,你就巴拉巴拉说了一堆。

这个问题该怎么解决呢?

地球人都知道:三次握手嘛!

  • A:我准备好了(SYN)
  • B:我知道了(ACK),我也准备好了(SYN)
  • A:我知道了(ACK)

网络编程入门从未如此简单(二):假如你来设计TCP协议,会怎么做?_8-2.gif

A 与 B 各自在内存中维护着自己的状态变量,三次握手之后,双方的状态都变成了连接已建立(ESTABLISHED)。

虽然就只是发了三次数据包,并且在各自的内存中维护了状态变量,但这么说总觉得太 low,你看这个过程相当于双方建立连接的过程,于是你灵机一动,就叫它面向连接吧。

注意:这个连接是虚拟的,是由 A 和 B 这两个终端共同维护的,在网络中的设备根本就不知道连接这回事儿!

但凡事有始就有终,有了建立连接的过程,就要考虑释放连接的过程。

这就是网络编程中耳熟能详的四次挥手啦!

  • A:再见,我要关闭了(FIN)
  • B:我知道了(ACK)。给 B 一段时间把自己的事情处理完...
  • B:再见,我要关闭了(FIN)
  • A:我知道了(ACK)

网络编程入门从未如此简单(二):假如你来设计TCP协议,会怎么做?_8-3.gif

11、小结一下

以上讲述的,就是 TCP 协议的核心思想,上面过程中需要传输的信息,就体现在 TCP 协议的头部,这里放上最常见的 TCP 协议头解读的图。

网络编程入门从未如此简单(二):假如你来设计TCP协议,会怎么做?_9-1.png

不知道你现在再看下面这句话,是否能理解:

TCP 是面向连接的、可靠的、基于字节流的传输层通信协议。

“面向连接、可靠”,这两个词通过上面的讲述很容易理解,那什么叫做基于字节流呢?

很简单:TCP 在建立连接时,需要告诉对方 MSS(最大报文段大小)。

也就是说:如果要发送的数据很大,在 TCP 层是需要按照 MSS 来切割成一个个的 TCP 报文段 的。

切割的时候我才不管你原来的数据表示什么意思,需要在哪里断句啥的,我就把它当成一串毫无意义的字节,在我想要切割的地方咔嚓就来一刀,标上序号,只要接收方再根据这个序号拼成最终想要的完整数据就行了。

在我 TCP 传输这里,我就把它当做一个个的字节,也就是基于字节流的含义了。

网络编程入门从未如此简单(二):假如你来设计TCP协议,会怎么做?_10-1.png

12、写在最后

一提到 TCP,可能很多人都想起被三次握手和四次挥手所支配的恐惧。

但其实你跟着本文中的思路你就会发现,三次握手与四次挥手只占 TCP 所解决的核心问题中很小的一部分,只是因为它在面试中很适合作为知识点进行考察,所以在很多人的印象中就好像 TCP 的核心就是握手和挥手似的。

本文希望你能从问题出发,真正理解 TCP 所想要解决的问题,你会发现很多原理就好像生活常识一样顺其自然,并不复杂,希望你有收获~

最后,如果对TCP的理解仍存在疑惑,可以继续阅读以下精选的资料:

本文已同步发布于“即时通讯技术圈”公众号。

▲ 本文在公众号上的链接是:点此进入。同步发布链接是:http://www.52im.net/thread-3339-1-1.html

posted @ 2021-02-24 12:47 Jack Jiang 阅读(68) | 评论 (0)编辑 收藏

2021年2月2日

     摘要: 本文原题“如果让你来设计网络”,由闪客sun分享,转载请联系作者。1、引言网络编程能力对于即时通讯技术开发者来说是基本功,而计算机网络又是网络编程的理论根基,因而深刻准确地理解计算机网络知识显然能夯实你的即时通讯应用的实践品质。本文风格类似于52im社区里的《网络编程懒人入门》、《脑残式网络编程入门》两个系列,但通俗又不失内涵,简洁又不简陋,非常适合对计算机网络知识有向往但...  阅读全文

posted @ 2021-02-02 15:24 Jack Jiang 阅读(99) | 评论 (0)编辑 收藏

2021年1月25日

本文原题“高并发高性能服务器是如何实现的”,转载请联系作者。

1、系列文章引言

1.1 文章目的

作为即时通讯技术的开发者来说,高性能、高并发相关的技术概念早就了然与胸,什么线程池、零拷贝、多路复用、事件驱动、epoll等等名词信手拈来,又或许你对具有这些技术特征的技术框架比如:Java的Netty、Php的workman、Go的gnet等熟练掌握。但真正到了面视或者技术实践过程中遇到无法释怀的疑惑时,方知自已所掌握的不过是皮毛。

返璞归真、回归本质,这些技术特征背后的底层原理到底是什么?如何能通俗易懂、毫不费力真正透彻理解这些技术背后的原理,正是《从根上理解高性能、高并发》系列文章所要分享的。

1.2 文章源起

我整理了相当多有关IM、消息推送等即时通讯技术相关的资源和文章,从最开始的开源IM框架MobileIMSDK,到网络编程经典巨著《TCP/IP详解》的在线版本,再到IM开发纲领性文章《新手入门一篇就够:从零开发移动端IM》,以及网络编程由浅到深的《网络编程懒人入门》、《脑残式网络编程入门》、《高性能网络编程》、《不为人知的网络编程》系列文章。

越往知识的深处走,越觉得对即时通讯技术了解的太少。于是后来,为了让开发者门更好地从基础电信技术的角度理解网络(尤其移动网络)特性,我跨专业收集整理了《IM开发者的零基础通信技术入门》系列高阶文章。这系列文章已然是普通即时通讯开发者的网络通信技术知识边界,加上之前这些网络编程资料,解决网络通信方面的知识盲点基本够用了。

对于即时通讯IM这种系统的开发来说,网络通信知识确实非常重要,但回归到技术本质,实现网络通信本身的这些技术特征:包括上面提到的线程池、零拷贝、多路复用、事件驱动等等,它们的本质是什么?底层原理又是怎样?这就是整理本系列文章的目的,希望对你有用。

1.3 文章目录

从根上理解高性能、高并发(一):深入计算机底层,理解线程与线程池

从根上理解高性能、高并发(二):深入操作系统,理解I/O与零拷贝技术

从根上理解高性能、高并发(三):深入操作系统,彻底理解I/O多路复用

从根上理解高性能、高并发(四):深入操作系统,彻底理解同步与异步

从根上理解高性能、高并发(五):深入操作系统,理解高并发中的协程

从根上理解高性能、高并发(六):通俗易懂,高性能服务器到底是如何实现的》(* 本文

1.4 本篇概述

接上篇《从根上理解高性能、高并发(五):深入操作系统,理解高并发中的协程》,本篇是高性能、高并发系列的第6篇文章(也是完结篇)。

本篇是本系列文章的完结篇,你将能了解到,一个典型的服务器端是如何利用前5篇中讲解的各单项技术从而实现高性能高并发的。

本文已同步发布于“即时通讯技术圈”公众号,欢迎关注。公众号上的链接是:点此进入

2、本文作者

应作者要求,不提供真名,也不提供个人照片。

本文作者主要技术方向为互联网后端、高并发高性能服务器、检索引擎技术,网名是“码农的荒岛求生”。感谢作者的无私分享。

3、正文引言

当你在阅读本篇文章的时候,有没有想过,服务器是怎么把这篇文章发送给你的呢?

说起来很简单:不就是一个用户请求吗?服务器根据请求从数据库中捞出这篇文章,然后通过网络发回去吗。

其实有点复杂:服务器端到底是如何并行处理成千上万个用户请求的呢?这里面又涉及到哪些技术呢?

这篇文章就是来为你解答这个问题的。

4、多进程

历史上最早出现也是最简单的一种并行处理多个请求的方法就是利用多进程

比如在Linux世界中,我们可以使用fork、exec等系统调用创建多个进程,我们可以在父进程中接收用户的连接请求,然后创建子进程去处理用户请求。

就像这样:

这种方法的优点就在于:

  • 1)编程简单,非常容易理解;
  • 2)由于各个进程的地址空间是相互隔离的,因此一个进程崩溃后并不会影响其它进程;
  • 3)充分利用多核资源。

多进程并行处理的优点很明显,但是缺点同样明显:

  • 1)各个进程地址空间相互隔离,这一优点也会变成缺点,那就是进程间要想通信就会变得比较困难,你需要借助进程间通信(IPC,interprocess communications)机制,想一想你现在知道哪些进程间通信机制,然后让你用代码实现呢?显然,进程间通信编程相对复杂,而且性能也是一大问题;
  • 2)我们知道创建进程开销是比线程要大的,频繁的创建销毁进程无疑会加重系统负担。

幸好,除了进程,我们还有线程。

5、多线程

不是创建进程开销大吗?不是进程间通信困难吗?这些对于线程来说统统不是问题。

什么?你还不了解线程,赶紧看看这篇《深入计算机底层,理解线程与线程池》,这里详细讲解了线程这个概念是怎么来的。

由于线程共享进程地址空间,因此线程间通信天然不需要借助任何通信机制,直接读取内存就好了。

线程创建销毁的开销也变小了,要知道线程就像寄居蟹一样,房子(地址空间)都是进程的,自己只是一个租客,因此非常的轻量级,创建销毁的开销也非常小。

我们可以为每个请求创建一个线程,即使一个线程因执行I/O操作——比如读取数据库等——被阻塞暂停运行也不会影响到其它线程。

就像这样:

但线程就是完美的、包治百病的吗,显然,计算机世界从来没有那么简单。

由于线程共享进程地址空间,这在为线程间通信带来便利的同时也带来了无尽的麻烦。

正是由于线程间共享地址空间,因此一个线程崩溃会导致整个进程崩溃退出,同时线程间通信简直太简单了,简单到线程间通信只需要直接读取内存就可以了,也简单到出现问题也极其容易,死锁、线程间的同步互斥、等等,这些极容易产生bug,无数程序员宝贵的时间就有相当一部分用来解决多线程带来的无尽问题。

虽然线程也有缺点,但是相比多进程来说,线程更有优势,但想单纯的利用多线程就能解决高并发问题也是不切实际的。

因为虽然线程创建开销相比进程小,但依然也是有开销的,对于动辄数万数十万的链接的高并发服务器来说,创建数万个线程会有性能问题,这包括内存占用、线程间切换,也就是调度的开销。

因此,我们需要进一步思考。

6、事件驱动:Event Loop

到目前为止,我们提到“并行”二字就会想到进程、线程。

但是:并行编程只能依赖这两项技术吗?并不是这样的!

还有另一项并行技术广泛应用在GUI编程以及服务器编程中,这就是近几年非常流行的事件驱动编程:event-based concurrency。

PS:搞IM服务端开发的程序员肯定不陌生,著名的Java NIO高性能网络编程框架Netty中EvenLoop 这个接口意味着什么(有关Netty框架的高性能原理可以读这篇《新手入门:目前为止最透彻的的Netty高性能原理和框架架构解析》)。

大家不要觉得这是一项很难懂的技术,实际上事件驱动编程原理上非常简单。

这一技术需要两种原料:

  • 1)event;
  • 2)处理event的函数,这一函数通常被称为event handler;

剩下的就简单了:你只需要安静的等待event到来就好,当event到来之后,检查一下event的类型,并根据该类型找到对应的event处理函数,也就是event handler,然后直接调用该event handler就好了。

That's it !

以上就是事件驱动编程的全部内容,是不是很简单!

从上面的讨论可以看到:我们需要不断的接收event然后处理event,因此我们需要一个循环(用while或者for循环都可以),这个循环被称为Event loop。

使用伪代码表示就是这样:

while(true) {

    event = getEvent();

    handler(event);

}

Event loop中要做的事情其实是非常简单的,只需要等待event的带来,然后调用相应的event处理函数即可。

注意:这段代码只需要运行在一个线程或者进程中,只需要这一个event loop就可以同时处理多个用户请求。

有的同学可以依然不明白:为什么这样一个event loop可以同时处理多个请求呢?

原因很简单:对于网络通信服务器来说,处理一个用户请求时大部分时间其实都用在了I/O操作上,像数据库读写、文件读写、网络读写等。当一个请求到来,简单处理之后可能就需要查询数据库等I/O操作,我们知道I/O是非常慢的,当发起I/O后我们大可以不用等待该I/O操作完成就可以继续处理接下来的用户请求。

现在你应该明白了吧:虽然上一个用户请求还没有处理完我们其实就可以处理下一个用户请求了,这也是并行,这种并行就可以用事件驱动编程来处理。

这就好比餐厅服务员一样:一个服务员不可能一直等上一个顾客下单、上菜、吃饭、买单之后才接待下一个顾客,服务员是怎么做的呢?当一个顾客下完单后直接处理下一个顾客,当顾客吃完饭后会自己回来买单结账的。

看到了吧:同样是一个服务员也可以同时处理多个顾客,这个服务员就相当于这里的Event loop,即使这个event loop只运行在一个线程(进程)中也可以同时处理多个用户请求。

相信你已经对事件驱动编程有一个清晰的认知了,那么接下来的问题就是,这个事件也就是event该怎么获取呢?

7、事件来源:IO多路复用

在《深入操作系统,彻底理解I/O多路复用》这篇文章中我们知道,在Linux/Unix世界中一切皆文件,而我们的程序都是通过文件描述符来进行I/O操作的,当然对于网络编程中的socket也不例外。

那我们该如何同时处理多个文件描述符呢?

IO多路复用技术正是用来解决这一问题的:通过IO多路复用技术,我们一次可以监控多个文件描述,当某个“文件”(实际可能是im网络通信中socket)可读或者可写的时候我们就能得到通知啦。

这样IO多路复用技术就成了event loop的原材料供应商,源源不断的给我们提供各种event,这样关于event来源的问题就解决了。

当然:关于IO多路复用技术的详细讲解请参见《深入操作系统,彻底理解I/O多路复用》,本文作为纲领性文章,就不再赘述了。

至此:关于利用事件驱动来实现并发编程的所有问题都解决了吗?event的来源问题解决了,当得到event后调用相应的handler,看上去大功告成了。

想一想还有没有其它问题?

8、问题:阻塞式IO

现在:我们可以使用一个线程(进程)就能基于事件驱动进行并行编程,再也没有了多线程中让人恼火的各种锁、同步互斥、死锁等问题了。

但是:计算机科学中从来没有出现过一种能解决所有问题的技术,现在没有,在可预期的将来也不会有。

那上述方法有什么问题吗?

不要忘了,我们event loop是运行在一个线程(进程),这虽然解决了多线程问题,但是如果在处理某个event时需要进行IO操作会怎么样呢?

在《深入操作系统,理解I/O与零拷贝技术》一文中,我们讲解了最常用的文件读取在底层是如何实现的,程序员最常用的这种IO方式被称为阻塞式IO。

也就是说:当我们进行IO操作,比如读取文件时,如果文件没有读取完成,那么我们的程序(线程)会被阻塞而暂停执行,这在多线程中不是问题,因为操作系统还可以调度其它线程。

但是:在单线程的event loop中是有问题的,原因就在于当我们在event loop中执行阻塞式IO操作时整个线程(event loop)会被暂停运行,这时操作系统将没有其它线程可以调度,因为系统中只有一个event loop在处理用户请求,这样当event loop线程被阻塞暂停运行时所有用户请求都没有办法被处理。你能想象当服务器在处理其它用户请求读取数据库导致你的请求被暂停吗?

因此:在基于事件驱动编程时有一条注意事项,那就是不允许发起阻塞式IO。

有的同学可能会问,如果不能发起阻塞式IO的话,那么该怎样进行IO操作呢?

PS:有阻塞式IO,就有非阻塞式IO。我们继续往下讨论。

9、解决方法:非阻塞式IO

为克服阻塞式IO所带来的问题,现代操作系统开始提供一种新的发起IO请求的方法,这种方法就是异步IO。对应的,阻塞式IO就是同步IO,关于同步和异步这两个概念可以参考《从根上理解高性能、高并发(四):深入操作系统,彻底理解同步与异步》。

异步IO时,假设调用aio_read函数(具体的异步IO API请参考具体的操作系统平台),也就是异步读取,当我们调用该函数后可以立即返回,并继续其它事情,虽然此时该文件可能还没有被读取,这样就不会阻塞调用线程了。此外,操作系统还会提供其它方法供调用线程来检测IO操作是否完成。

就这样,在操作系统的帮助下IO的阻塞调用问题也解决了。

10、基于事件驱动并行编程的难点

虽然有异步IO来解决event loop可能被阻塞的问题,但是基于事件编程依然是困难的。

首先:我们提到,event loop是运行在一个线程中的,显然一个线程是没有办法充分利用多核资源的,有的同学可能会说那就创建多个event loop实例不就可以了,这样就有多个event loop线程了,但是这样一来多线程问题又会出现。

另一点在于编程方面,在《从根上理解高性能、高并发(四):深入操作系统,彻底理解同步与异步》这篇文章中我们讲到过,异步编程需要结合回调函数(这种编程方式需要把处理逻辑分为两部分:一部分调用方自己处理,另一部分在回调函数中处理),这一编程方式的改变加重了程序员在理解上的负担,基于事件编程的项目后期会很难扩展以及维护。

那么有没有更好的方法呢?

要找到更好的方法,我们需要解决问题的本质,那么这个本质问题是什么呢?

11、更好的方法

为什么我们要使用异步这种难以理解的方式编程呢?

是因为:阻塞式编程虽然容易理解但会导致线程被阻塞而暂停运行。

那么聪明的你一定会问了:有没有一种方法既能结合同步IO的简单理解又不会因同步调用导致线程被阻塞呢?

答案是肯定的:这就是用户态线程(user level thread),也就是大名鼎鼎的协程(关于协程请详读本系列的上篇《从根上理解高性能、高并发(五):深入操作系统,理解高并发中的协程》,本文就不再赘述了)。

虽然基于事件编程有这样那样的缺点,但是在当今的高性能高并发服务器上基于事件编程方式依然非常流行,但已经不是纯粹的基于单一线程的事件驱动了,而是 event loop + multi thread + user level thread。

关于这一组合,同样值得拿出一篇文章来讲解,我们将在后续文章中详细讨论。

12、本文小结

高并发技术从最开始的多进程一路演进到当前的事件驱动,计算机技术就像生物一样也在不断演变进化,但不管怎样,了解历史才能更深刻的理解当下。希望这篇文章能对大家理解高并发服务器有所帮助。

附录:更多高性能、高并发文章精选

高性能网络编程(一):单台服务器并发TCP连接数到底可以有多少

高性能网络编程(二):上一个10年,著名的C10K并发连接问题

高性能网络编程(三):下一个10年,是时候考虑C10M并发问题了

高性能网络编程(四):从C10K到C10M高性能网络应用的理论探索

高性能网络编程(五):一文读懂高性能网络编程中的I/O模型

高性能网络编程(六):一文读懂高性能网络编程中的线程模型

高性能网络编程(七):到底什么是高并发?一文即懂!

以网游服务端的网络接入层设计为例,理解实时通信的技术挑战

知乎技术分享:知乎千万级并发的高性能长连接网关技术实践

淘宝技术分享:手淘亿级移动端接入层网关的技术演进之路

一套海量在线用户的移动端IM架构设计实践分享(含详细图文)

一套原创分布式即时通讯(IM)系统理论架构方案

微信后台基于时间序的海量数据冷热分级架构设计实践

微信技术总监谈架构:微信之道——大道至简(演讲全文)

如何解读《微信技术总监谈架构:微信之道——大道至简》

快速裂变:见证微信强大后台架构从0到1的演进历程(一)

17年的实践:腾讯海量产品的技术方法论

腾讯资深架构师干货总结:一文读懂大型分布式系统设计的方方面面

以微博类应用场景为例,总结海量社交系统的架构设计步骤

新手入门:零基础理解大型分布式架构的演进历史、技术原理、最佳实践

从新手到架构师,一篇就够:从100到1000万高并发的架构演进之路

本文已同步发布于“即时通讯技术圈”公众号。

▲ 本文在公众号上的链接是:点此进入。同步发布链接是:http://www.52im.net/thread-3315-1-1.html

posted @ 2021-01-25 16:36 Jack Jiang 阅读(157) | 评论 (0)编辑 收藏

2021年1月18日

     摘要: 本文原题“程序员应如何理解高并发中的协程”,转载请联系作者。1、系列文章引言1.1 文章目的作为即时通讯技术的开发者来说,高性能、高并发相关的技术概念早就了然与胸,什么线程池、零拷贝、多路复用、事件驱动、epoll等等名词信手拈来,又或许你对具有这些技术特征的技术框架比如:Java的Netty、Php的workman、Go的gnet等熟练掌握。但真正到了面视或者技术实践过程...  阅读全文

posted @ 2021-01-18 14:51 Jack Jiang 阅读(99) | 评论 (0)编辑 收藏

2021年1月12日

     摘要: 本文原题“从小白到高手,你需要理解同步与异步”,转载请联系作者。1、系列文章引言1.1 文章目的作为即时通讯技术的开发者来说,高性能、高并发相关的技术概念早就了然与胸,什么线程池、零拷贝、多路复用、事件驱动、epoll等等名词信手拈来,又或许你对具有这些技术特征的技术框架比如:Java的Netty、Php的workman、Go的gnet等熟练掌握。但真正到了面视或者技术实践...  阅读全文

posted @ 2021-01-12 14:15 Jack Jiang 阅读(107) | 评论 (0)编辑 收藏

2021年1月5日

本文原题“终于明白了,一文彻底理解I/O多路复用”,转载请联系作者。

1、系列文章引言

1.1 文章目的

作为即时通讯技术的开发者来说,高性能、高并发相关的技术概念早就了然与胸,什么线程池、零拷贝、多路复用、事件驱动、epoll等等名词信手拈来,又或许你对具有这些技术特征的技术框架比如:Java的Netty、Php的workman、Go的nget等熟练掌握。但真正到了面视或者技术实践过程中遇到无法释怀的疑惑时,方知自已所掌握的不过是皮毛。

返璞归真、回归本质,这些技术特征背后的底层原理到底是什么?如何能通俗易懂、毫不费力真正透彻理解这些技术背后的原理,正是《从根上理解高性能、高并发》系列文章所要分享的。

1.2 文章源起

我整理了相当多有关IM、消息推送等即时通讯技术相关的资源和文章,从最开始的开源IM框架MobileIMSDK,到网络编程经典巨著《TCP/IP详解》的在线版本,再到IM开发纲领性文章《新手入门一篇就够:从零开发移动端IM》,以及网络编程由浅到深的《网络编程懒人入门》、《脑残式网络编程入门》、《高性能网络编程》、《不为人知的网络编程》系列文章。

越往知识的深处走,越觉得对即时通讯技术了解的太少。于是后来,为了让开发者门更好地从基础电信技术的角度理解网络(尤其移动网络)特性,我跨专业收集整理了《IM开发者的零基础通信技术入门》系列高阶文章。这系列文章已然是普通即时通讯开发者的网络通信技术知识边界,加上之前这些网络编程资料,解决网络通信方面的知识盲点基本够用了。

对于即时通讯IM这种系统的开发来说,网络通信知识确实非常重要,但回归到技术本质,实现网络通信本身的这些技术特征:包括上面提到的线程池、零拷贝、多路复用、事件驱动等等,它们的本质是什么?底层原理又是怎样?这就是整理本系列文章的目的,希望对你有用。

1.3 文章目录

从根上理解高性能、高并发(一):深入计算机底层,理解线程与线程池

从根上理解高性能、高并发(二):深入操作系统,理解I/O与零拷贝技术

从根上理解高性能、高并发(三):深入操作系统,彻底理解I/O多路复用》(* 本文

《从根上理解高性能、高并发(四):深入操作系统,彻底理解同步与异步 (稍后发布..)》

《从根上理解高性能、高并发(五):高并发高性能服务器到底是如何实现的 (稍后发布..)》

1.4 本篇概述

接上篇《深入操作系统,理解I/O与零拷贝技术》,本篇是高性能、高并发系列的第3篇文章,上篇里我们讲到了I/O技术,本篇将以更具象的文件这个话题入手,带你一步步理解高性能、高并发服务端编程时无法回避的I/O多路复用及相关技术。

本文已同步发布于“即时通讯技术圈”公众号,欢迎关注。公众号上的链接是:点此进入

2、本文作者

应作者要求,不提供真名,也不提供个人照片。

本文作者主要技术方向为互联网后端、高并发高性能服务器、检索引擎技术,网名是“码农的荒岛求生”,公众号“码农的荒岛求生”。感谢作者的无私分享。

3、什么是文件?

在正式展开本文的内容之前,我们需要先预习一下文件以及文件描述符的概念。

程序员使用I/O最终都逃不过文件这个概念。

在Linux世界中文件是一个很简单的概念,作为程序员我们只需要将其理解为一个N byte的序列就可以了:

b1, b2, b3, b4, ....... bN

实际上所有的I/O设备都被抽象为了文件这个概念,一切皆文件(Everything is File),磁盘、网络数据、终端,甚至进程间通信工具管道pipe等都被当做文件对待。

所有的I/O操作也都可以通过文件读写来实现,这一非常优雅的抽象可以让程序员使用一套接口就能对所有外设I/O操作。

常用的I/O操作接口一般有以下几类:

  • 1)打开文件,open;
  • 2)改变读写位置,seek;
  • 3)文件读写,read、write;
  • 4)关闭文件,close。

程序员通过这几个接口几乎可以实现所有I/O操作,这就是文件这个概念的强大之处。

4、什么是文件描述符?

在上一篇《深入操作系统,理解I/O与零拷贝技术》中我们讲到:要想进行I/O读操作,像磁盘数据,我们需要指定一个buff用来装入数据。

一般都是这样写的:

read(buff);

但是这里我们忽略了一个关键问题:那就是,虽然我们指定了往哪里写数据,但是我们该从哪里读数据呢?

从上一节中我们知道,通过文件这个概念我们能实现几乎所有I/O操作,因此这里少的一个主角就是文件。

那么我们一般都怎样使用文件呢?

举个例子:如果周末你去比较火的餐厅吃饭应该会有体会,一般周末人气高的餐厅都会排队,然后服务员会给你一个排队序号,通过这个序号服务员就能找到你,这里的好处就是服务员无需记住你是谁、你的名字是什么、来自哪里、喜好是什么、是不是保护环境爱护小动物等等,这里的关键点就是:服务员对你一无所知,但依然可以通过一个号码就能找到你。

同样的:在Linux世界要想使用文件,我们也需要借助一个号码,根据“弄不懂原则”,这个号码就被称为了文件描述符(file descriptors),在Linux世界中鼎鼎大名,其道理和上面那个排队号码一样。

因此:文件描述仅仅就是一个数字而已,但是通过这个数字我们可以操作一个打开的文件,这一点要记住。

有了文件描述符,进程可以对文件一无所知,比如文件在磁盘的什么位置、加载到内存中又是怎样管理的等等,这些信息统统交由操作系统打理,进程无需关心,操作系统只需要给进程一个文件描述符就足够了。

因此我们来完善上述程序:

int fd = open(file_name); // 获取文件描述符

read(fd, buff);

怎么样,是不是非常简单。

5、文件描述符太多了怎么办?

经过了这么多的铺垫,终于要到高性能、高并发这一主题了。

从前几节我们知道,所有I/O操作都可以通过文件样的概念来进行,这当然包括网络通信。

如果你有一个IM服务器,当三次握手建议长连接成功以后,我们会调用accept来获取一个链接,调用该函数我们同样会得到一个文件描述符,通过这个文件描述符就可以处理客户端发送的聊天消息并且把消息转发给接收者。

也就是说,通过这个描述符我们就可以和客户端进行通信了:

// 通过accept获取客户端的文件描述符

int conn_fd = accept(...);

Server端的处理逻辑通常是接收客户端消息数据,然后执行转发(给接收者)逻辑:

if(read(conn_fd, msg_buff) > 0) {

do_transfer(msg_buff);

}

是不是非常简单,然而世界终归是复杂的,当然也不是这么简单的。

接下来就是比较复杂的了。

既然我们的主题是高并发,那么Server端就不可能只和一个客户端通信,而是可能会同时和成千上万个客户端进行通信。这时你需要处理不再是一个描述符这么简单,而是有可能要处理成千上万个描述符。

为了不让问题一上来就过于复杂,我们先简单化,假设只同时处理两个客户端的请求。

有的同学可能会说,这还不简单,这样写不就行了:

if(read(socket_fd1, buff) > 0) { // 处理第一个

do_transfer();

}

if(read(socket_fd2, buff) > 0) { // 处理第二个

do_transfer();

在上一篇《深入操作系统,理解I/O与零拷贝技术》中我们讨论过,这是非常典型的阻塞式I/O,如果此时没有数据可读那么进程会被阻塞而暂停运行。这时我们就无法处理第二个请求了,即使第二个请求的数据已经就位,这也就意味着处理某一个客户端时由于进程被阻塞导致剩下的所有其它客户端必须等待,在同时处理几万客户端的server上。这显然是不能容忍的。

聪明的你一定会想到使用多线程:为每个客户端请求开启一个线程,这样一个客户端被阻塞就不会影响到处理其它客户端的线程了。注意:既然是高并发,那么我们要为成千上万个请求开启成千上万个线程吗,大量创建销毁线程会严重影响系统性能。

那么这个问题该怎么解决呢?

这里的关键点在于:我们事先并不知道一个文件描述对应的I/O设备是否是可读的、是否是可写的,在外设的不可读或不可写的状态下进行I/O只会导致进程阻塞被暂停运行。

因此要优雅的解决这个问题,就要从其它角度来思考这个问题了。

6、“不要打电话给我,有需要我会打给你”

大家生活中肯定会接到过推销电话,而且不止一个,一天下来接上十个八个推销电话你的身体会被掏空的。

这个场景的关键点在于:打电话的人并不知道你是不是要买东西,只能来一遍遍问你。因此一种更好的策略是不要让他们打电话给你,记下他们的电话,有需要的话打给他们,这样推销员就不会一遍一遍的来烦你了(虽然现实生活中这并不可能)。

在这个例子中:你,就好比内核,推销者就好比应用程序,电话号码就好比文件描述符,和你用电话沟通就好比I/O。

现在你应该明白了吧,处理多个文件描述符的更好方法其实就存在于推销电话中。

因此相比上一节中:我们通过I/O接口主动问内核这些文件描述符对应的外设是不是已经就绪了,一种更好的方法是,我们把这些感兴趣的文件描述符一股脑扔给内核,并霸气的告诉内核:“我这里有1万个文件描述符,你替我监视着它们,有可以读写的文件描述符时你就告诉我,我好处理”。而不是弱弱的问内核:“第一个文件描述可以读写了吗?第二个文件描述符可以读写吗?第三个文件描述符可以读写了吗?。。。”

这样:应用程序就从“繁忙”的主动变为了清闲的被动,反正文件描述可读可写了内核会通知我,能偷懒我才不要那么勤奋。

这是一种更加高效的I/O处理机制,现在我们可以一次处理多路I/O了,为这种机制起一个名字吧,就叫I/O多路复用吧,这就是 I/O multiplexing。

7、I/O多路复用(I/O multiplexing)

multiplexing一词其实多用于通信领域,为了充分利用通信线路,希望在一个信道中传输多路信号,要想在一个信道中传输多路信号就需要把这多路信号结合为一路,将多路信号组合成一个信号的设备被称为Multiplexer(多路复用器),显然接收方接收到这一路组合后的信号后要恢复原先的多路信号,这个设备被称为Demultiplexer(多路分用器)。

如下图所示:

回到我们的主题。

所谓I/O多路复用指的是这样一个过程:

  • 1)我们拿到了一堆文件描述符(不管是网络相关的、还是磁盘文件相关等等,任何文件描述符都可以);
  • 2)通过调用某个函数告诉内核:“这个函数你先不要返回,你替我监视着这些描述符,当这堆文件描述符中有可以进行I/O读写操作的时候你再返回”;
  • 3)当调用的这个函数返回后我们就能知道哪些文件描述符可以进行I/O操作了。

也就是说通过I/O多路复用我们可以同时处理多路I/O。那么有哪些函数可以用来进行I/O多路复用呢?

以Linux为例,有这样三种机制可以用来进行I/O多路复用:

  • 1)select;
  • 2)poll;
  • 3)epoll。

接下来我们就来介绍一下牛掰的I/O多路复用三剑客。

8、I/O多路复用三剑客

本质上:Linux上的select、poll、epoll都是阻塞式I/O,也就是我们常说的同步I/O。

原因在于:调用这些I/O多路复用函数时如果任何一个需要监视的文件描述符都不可读或者可写那么进程会被阻塞暂停执行,直到有文件描述符可读或者可写才继续运行。

8.1 select:初出茅庐

在select这种I/O多路复用机制下,我们需要把想监控的文件描述集合通过函数参数的形式告诉select,然后select会将这些文件描述符集合拷贝到内核中。

我们知道数据拷贝是有性能损耗的,因此为了减少这种数据拷贝带来的性能损耗,Linux内核对集合的大小做了限制,并规定用户监控的文件描述集合不能超过1024个,同时当select返回后我们仅仅能知道有些文件描述符可以读写了,但是我们不知道是哪一个。因此程序员必须再遍历一边找到具体是哪个文件描述符可以读写了。

因此,总结下来select有这样几个特点:

  • 1)我能照看的文件描述符数量有限,不能超过1024个;
  • 2)用户给我的文件描述符需要拷贝的内核中;
  • 3)我只能告诉你有文件描述符满足要求了,但是我不知道是哪个,你自己一个一个去找吧(遍历)。

因此我们可以看到,select机制的这些特性在高并发网络服务器动辄几万几十万并发链接的场景下无疑是低效的。

8.2 poll:小有所成

poll和select是非常相似的。

poll相对于select的优化仅仅在于解决了文件描述符不能超过1024个的限制,select和poll都会随着监控的文件描述数量增加而性能下降,因此不适合高并发场景。

8.3 epoll:独步天下

在select面临的三个问题中,文件描述数量限制已经在poll中解决了,剩下的两个问题呢?

针对拷贝问题:epoll使用的策略是各个击破与共享内存。

实际上:文件描述符集合的变化频率比较低,select和poll频繁的拷贝整个集合,内核都快被烦死了,epoll通过引入epoll_ctl很体贴的做到了只操作那些有变化的文件描述符。同时epoll和内核还成为了好朋友,共享了同一块内存,这块内存中保存的就是那些已经可读或者可写的的文件描述符集合,这样就减少了内核和程序的拷贝开销。

针对需要遍历文件描述符才能知道哪个可读可写这一问题,epoll使用的策略是“当小弟”。

在select和poll机制下:进程要亲自下场去各个文件描述符上等待,任何一个文件描述可读或者可写就唤醒进程,但是进程被唤醒后也是一脸懵逼并不知道到底是哪个文件描述符可读或可写,还要再从头到尾检查一遍。

但epoll就懂事多了,主动找到进程要当小弟替大哥出头。

在这种机制下:进程不需要亲自下场了,进程只要等待在epoll上,epoll代替进程去各个文件描述符上等待,当哪个文件描述符可读或者可写的时候就告诉epoll,epoll用小本本认真记录下来然后唤醒大哥:“进程大哥,快醒醒,你要处理的文件描述符我都记下来了”,这样进程被唤醒后就无需自己从头到尾检查一遍,因为epoll小弟都已经记下来了。

因此我们可以看到:在epoll这种机制下,实际上利用的就是“不要打电话给我,有需要我会打给你”这种策略,进程不需要一遍一遍麻烦的问各个文件描述符,而是翻身做主人了——“你们这些文件描述符有哪个可读或者可写了主动报上来”。

这种机制实际上就是大名鼎鼎的事件驱动——Event-driven,这也是我们下一篇的主题。

实际上:在Linux平台,epoll基本上就是高并发的代名词。

9、本文小结

基于一切皆文件的设计哲学,I/O也可以通过文件的形式实现,高并发场景下要与多个文件交互,这就离不开高效的I/O多路复用技术。

本文我们详细讲解了什么是I/O多路复用以及使用方法,这其中以epoll为代表的I/O多路复用(基于事件驱动)技术使用非常广泛,实际上你会发现但凡涉及到高并发、高性能的场景基本上都能见到事件驱动的编程方法,当然这也是下一篇我们要重点讲解的主题《从根上理解高性能、高并发(四):深入操作系统,彻底理解同步与异步》,敬请期待!

附录:更多高性能、高并发文章精选

高性能网络编程(一):单台服务器并发TCP连接数到底可以有多少

高性能网络编程(二):上一个10年,著名的C10K并发连接问题

高性能网络编程(三):下一个10年,是时候考虑C10M并发问题了

高性能网络编程(四):从C10K到C10M高性能网络应用的理论探索

高性能网络编程(五):一文读懂高性能网络编程中的I/O模型

高性能网络编程(六):一文读懂高性能网络编程中的线程模型

高性能网络编程(七):到底什么是高并发?一文即懂!

以网游服务端的网络接入层设计为例,理解实时通信的技术挑战

知乎技术分享:知乎千万级并发的高性能长连接网关技术实践

淘宝技术分享:手淘亿级移动端接入层网关的技术演进之路

一套海量在线用户的移动端IM架构设计实践分享(含详细图文)

一套原创分布式即时通讯(IM)系统理论架构方案

微信后台基于时间序的海量数据冷热分级架构设计实践

微信技术总监谈架构:微信之道——大道至简(演讲全文)

如何解读《微信技术总监谈架构:微信之道——大道至简》

快速裂变:见证微信强大后台架构从0到1的演进历程(一)

17年的实践:腾讯海量产品的技术方法论

腾讯资深架构师干货总结:一文读懂大型分布式系统设计的方方面面

以微博类应用场景为例,总结海量社交系统的架构设计步骤

新手入门:零基础理解大型分布式架构的演进历史、技术原理、最佳实践

从新手到架构师,一篇就够:从100到1000万高并发的架构演进之路

本文已同步发布于“即时通讯技术圈”公众号。

▲ 本文在公众号上的链接是:点此进入。同步发布链接是:http://www.52im.net/thread-3287-1-1.html

posted @ 2021-01-05 15:06 Jack Jiang 阅读(129) | 评论 (0)编辑 收藏

2020年12月28日

1、系列文章引言

1.1 文章目的

作为即时通讯技术的开发者来说,高性能、高并发相关的技术概念早就了然与胸,什么线程池、零拷贝、多路复用、事件驱动、epoll等等名词信手拈来,又或许你对具有这些技术特征的技术框架比如:Java的Netty、Php的workman、Go的nget等熟练掌握。但真正到了面视或者技术实践过程中遇到无法释怀的疑惑时,方知自已所掌握的不过是皮毛。

返璞归真、回归本质,这些技术特征背后的底层原理到底是什么?如何能通俗易懂、毫不费力真正透彻理解这些技术背后的原理,正是《从根上理解高性能、高并发》系列文章所要分享的。

1.2 文章源起

我整理了相当多有关IM、消息推送等即时通讯技术相关的资源和文章,从最开始的开源IM框架MobileIMSDK,到网络编程经典巨著《TCP/IP详解》的在线版本,再到IM开发纲领性文章《新手入门一篇就够:从零开发移动端IM》,以及网络编程由浅到深的《网络编程懒人入门》、《脑残式网络编程入门》、《高性能网络编程》、《不为人知的网络编程》系列文章。

越往知识的深处走,越觉得对即时通讯技术了解的太少。于是后来,为了让开发者门更好地从基础电信技术的角度理解网络(尤其移动网络)特性,我跨专业收集整理了《IM开发者的零基础通信技术入门》系列高阶文章。这系列文章已然是普通即时通讯开发者的网络通信技术知识边界,加上之前这些网络编程资料,解决网络通信方面的知识盲点基本够用了。

对于即时通讯IM这种系统的开发来说,网络通信知识确实非常重要,但回归到技术本质,实现网络通信本身的这些技术特征:包括上面提到的线程池、零拷贝、多路复用、事件驱动等等,它们的本质是什么?底层原理又是怎样?这就是整理本系列文章的目的,希望对你有用。

1.3 文章目录

1.4 本篇概述

接上篇《深入计算机底层,理解线程与线程池》,本篇是高性能、高并发系列的第2篇文章,在这里我们来到了I/O这一话题。你有没有想过,当我们执行文件I/O、网络I/O操作时计算机底层到底发生了些什么?对于计算机来说I/O是极其重要的,本篇将带给你这个问的答案。

2、本文作者

应作者要求,不提供真名,也不提供个人照片。

本文作者主要技术方向为互联网后端、高并发高性能服务器、检索引擎技术,网名是“码农的荒岛求生”,公众号“码农的荒岛求生”。感谢作者的无私分享。

3、不能执行I/O的计算机是什么?

相信对于程序员来说I/O操作是最为熟悉不过的了,比如:

  • 1)当我们使用C语言中的printf、C++中的"<<",Python中的print,Java中的System.out.println等时;
  • 2)当我们使用各种语言读写文件时;
  • 3)当我们通过TCP/IP进行网络通信时;
  • 4)当我们使用鼠标龙飞凤舞时;
  • 5)当我们拿起键盘在评论区指点江山亦或是埋头苦干努力制造bug时;
  • 6)当我们能看到屏幕上的漂亮的图形界面时等等。

以上这一切,都是I/O!

想一想:如果没有I/O计算机该是一种多么枯燥的设备,不能看电影、不能玩游戏,也不能上网,这样的计算机最多就是一个大号的计算器。

既然I/O这么重要,那么到底什么才是I/O呢?

4、什么是I/O?

I/O就是简单的数据Copy,仅此而已!

这一点很重要!

既然是copy数据,那么又是从哪里copy到哪里呢?

如果数据是从外部设备copy到内存中,这就是Input。

如果数据是从内存copy到外部设备,这就是Output。

内存与外部设备之间不嫌麻烦的来回copy数据就是Input and Output,简称I/O(Input/Output),仅此而已。

5、I/O与CPU

现在我们知道了什么是I/O,接下来就是重点部分了,大家注意,坐稳了。

我们知道现在的CPU其主频都是数GHz起步,这是什么意思呢?

简单说就是:CPU执行机器指令的速度是纳秒级别的,而通常的I/O比如磁盘操作,一次磁盘seek大概在毫秒级别,因此如果我们把CPU的速度比作战斗机的话,那么I/O操作的速度就是肯德鸡。

也就是说当我们的程序跑起来时(CPU执行机器指令),其速度是要远远快于I/O速度的。那么接下来的问题就是二者速度相差这么大,那么我们该如何设计、该如何更加合理的高效利用系统资源呢?

既然有速度差异,而且进程在执行完I/O操作前不能继续向前推进,那么显然只有一个办法,那就是等待(wait)。

同样是等待,有聪明的等待,也有傻傻的等待,简称傻等,那么是选择聪明的等待呢还是选择傻等呢?

假设你是一个急性子(CPU),需要等待一个重要的文件,不巧的是这个文件只能快递过来(I/O),那么这时你是选择什么事情都不干了,深情的注视着门口就像盼望着你的哈尼一样专心等待这个快递呢?还是暂时先不要管快递了,玩个游戏看个电影刷会儿短视频等快递来了再说呢?

很显然,更好的方法就是先去干其它事情,快递来了再说。

因此:这里的关键点就是快递没到前手头上的事情可以先暂停,切换到其它任务,等快递过来了再切换回来。

理解了这一点你就能明白执行I/O操作时底层都发生了什么。

接下来让我们以读取磁盘文件为例来讲解这一过程。

6、执行I/O时底层都发生了什么

在上一篇《深入计算机底层,理解线程与线程池》中,我们引入了进程和线程的概念。

在支持线程的操作系统中,实际上被调度的是线程而不是进程,为了更加清晰的理解I/O过程,我们暂时假设操作系统只有进程这样的概念,先不去考虑线程,这并不会影响我们的讨论。

现在内存中有两个进程,进程A和进程B,当前进程A正在运行。

如下图所示:

进程A中有一段读取文件的代码,不管在什么语言中通常我们定义一个用来装数据的buff,然后调用read之类的函数。

就像这样:

read(buff);

这就是一种典型的I/O操作,当CPU执行到这段代码的时候会向磁盘发送读取请求。

注意:与CPU执行指令的速度相比,I/O操作操作是非常慢的,因此操作系统是不可能把宝贵的CPU计算资源浪费在无谓的等待上的,这时重点来了,注意接下来是重点哦。

由于外部设备执行I/O操作是相当慢的,因此在I/O操作完成之前进程是无法继续向前推进的,这就是所谓的阻塞,即通常所说的block。

操作系统检测到进程向I/O设备发起请求后就暂停进程的运行,怎么暂停运行呢?很简单:只需要记录下当前进程的运行状态并把CPU的PC寄存器指向其它进程的指令就可以了。

进程有暂停就会有继续执行,因此操作系统必须保存被暂停的进程以备后续继续执行,显然我们可以用队列来保存被暂停执行的进程。

如下图所示,进程A被暂停执行并被放到阻塞队列中(注意:不同的操作系统会有不同的实现,可能每个I/O设备都有一个对应的阻塞队列,但这种实现细节上的差异不影响我们的讨论)。

这时操作系统已经向磁盘发送了I/O请求,因此磁盘driver开始将磁盘中的数据copy到进程A的buff中。虽然这时进程A已经被暂停执行了,但这并不妨碍磁盘向内存中copy数据。

注意:现代磁盘向内存copy数据时无需借助CPU的帮助,这就是所谓的DMA(Direct Memory Access)。

这个过程如下图所示:

让磁盘先copy着数据,我们接着聊。

实际上:操作系统中除了有阻塞队列之外也有就绪队列,所谓就绪队列是指队列里的进程准备就绪可以被CPU执行了。

你可能会问为什么不直接执行非要有个就绪队列呢?答案很简单:那就是僧多粥少,在即使只有1个核的机器上也可以创建出成千上万个进程,CPU不可能同时执行这么多的进程,因此必然存在这样的进程,即使其一切准备就绪也不能被分配到计算资源,这样的进程就被放到了就绪队列。

现在进程B就位于就绪队列,万事俱备只欠CPU。

如下图所示:

当进程A被暂停执行后CPU是不可以闲下来的,因为就绪队列中还有嗷嗷待哺的进程B,这时操作系统开始在就绪队列中找下一个可以执行的进程,也就是这里的进程B。

此时操作系统将进程B从就绪队列中取出,找出进程B被暂停时执行到的机器指令的位置,然后将CPU的PC寄存器指向该位置,这样进程B就开始运行啦。

如下图所示:

注意:接下来的这段是重点中的重点!

注意观察上图:此时进程B在被CPU执行,磁盘在向进程A的内存空间中copy数据,看出来了吗——大家都在忙,谁都没有闲着,数据copy和指令执行在同时进行,在操作系统的调度下,CPU、磁盘都得到了充分的利用,这就是程序员的智慧所在。

现在你应该理解为什么操作系统这么重要了吧。

此后磁盘终于将全部数据都copy到了进程A的内存中,这时磁盘通知操作系统任务完成啦,你可能会问怎么通知呢?这就是中断。

操作系统接收到磁盘中断后发现数据copy完毕,进程A重新获得继续运行的资格,这时操作系统小心翼翼的把进程A从阻塞队列放到了就绪队列当中。

如下图所示:

注意:从前面关于就绪状态的讨论中我们知道,操作系统是不会直接运行进程A的,进程A必须被放到就绪队列中等待,这样对大家都公平。

此后进程B继续执行,进程A继续等待,进程B执行了一会儿后操作系统认为进程B执行的时间够长了,因此把进程B放到就绪队列,把进程A取出并继续执行。

注意:操作系统把进程B放到的是就绪队列,因此进程B被暂停运行仅仅是因为时间片到了而不是因为发起I/O请求被阻塞。

如下图所示:

进程A继续执行,此时buff中已经装满了想要的数据,进程A就这样愉快的运行下去了,就好像从来没有被暂停过一样,进程对于自己被暂停一事一无所知,这就是操作系统的魔法。

现在你应该明白了I/O是一个怎样的过程了吧。

这种进程执行I/O操作被阻塞暂停执行的方式被称为阻塞式I/O,blocking I/O,这也是最常见最容易理解的I/O方式,有阻塞式I/O就有非阻塞式I/O,在这里我们暂时先不考虑这种方式。

在本节开头我们说过暂时只考虑进程而不考虑线程,现在我们放宽这个条件,实际上也非常简单,只需要把前图中调度的进程改为线程就可以了,这里的讨论对于线程一样成立。

7、零拷贝(Zero-copy)

最后需要注意的一点就是:上面的讲解中我们直接把磁盘数据copy到了进程空间中,但实际上一般情况下I/O数据是要首先copy到操作系统内部,然后操作系统再copy到进程空间中。

因此我们可以看到这里其实还有一层经过操作系统的copy,对于性能要求很高的场景其实也是可以绕过操作系统直接进行数据copy的,这也是本文描述的场景,这种绕过操作系统直接进行数据copy的技术被称为Zero-copy,也就零拷贝,高并发、高性能场景下常用的一种技术,原理上很简单吧。

PS:对于搞即时通讯开发的Java程序员来说,著名的高性能网络框架Netty就使用了零拷贝技术,具体可以读《NIO框架详解:Netty的高性能之道》一文的第12节。如果对于Netty框架很好奇但不了解的话,可以因着这两篇文章入门:《新手入门:目前为止最透彻的的Netty高性能原理和框架架构解析》、《史上最通俗Netty入门长文:基本介绍、环境搭建、动手实战》。

8、本文小结

本文讲解的是程序员常用的I/O(包括所谓的网络I/O),一般来说作为程序员我们无需关心,但是理解I/O背后的底层原理对于设计比如IM这种高性能、高并发系统是极为有益的,希望这篇能对大家加深对I/O的认识有所帮助。

接下来的一篇《从根上理解高性能、高并发(三):深入操作系统,彻底理解I/O多路复用》将要分享的是I/O技术的一大突破,正是因为它,才彻底解决了高并发网络通信中的C10K问题(见《高性能网络编程(二):上一个10年,著名的C10K并发连接问题),敬请期待!

附录:更多高性能、高并发文章精选

高性能网络编程(一):单台服务器并发TCP连接数到底可以有多少

高性能网络编程(二):上一个10年,著名的C10K并发连接问题

高性能网络编程(三):下一个10年,是时候考虑C10M并发问题了

高性能网络编程(四):从C10K到C10M高性能网络应用的理论探索

高性能网络编程(五):一文读懂高性能网络编程中的I/O模型

高性能网络编程(六):一文读懂高性能网络编程中的线程模型

高性能网络编程(七):到底什么是高并发?一文即懂!

以网游服务端的网络接入层设计为例,理解实时通信的技术挑战

知乎技术分享:知乎千万级并发的高性能长连接网关技术实践

淘宝技术分享:手淘亿级移动端接入层网关的技术演进之路

一套海量在线用户的移动端IM架构设计实践分享(含详细图文)

一套原创分布式即时通讯(IM)系统理论架构方案

微信后台基于时间序的海量数据冷热分级架构设计实践

微信技术总监谈架构:微信之道——大道至简(演讲全文)

如何解读《微信技术总监谈架构:微信之道——大道至简》

快速裂变:见证微信强大后台架构从0到1的演进历程(一)

17年的实践:腾讯海量产品的技术方法论

腾讯资深架构师干货总结:一文读懂大型分布式系统设计的方方面面

以微博类应用场景为例,总结海量社交系统的架构设计步骤

新手入门:零基础理解大型分布式架构的演进历史、技术原理、最佳实践

从新手到架构师,一篇就够:从100到1000万高并发的架构演进之路

本文已同步发布于“即时通讯技术圈”公众号。

▲ 本文在公众号上的链接是:点此进入。同步发布链接是:http://www.52im.net/thread-3280-1-1.html

posted @ 2020-12-28 14:42 Jack Jiang 阅读(119) | 评论 (0)编辑 收藏

2020年12月23日

     摘要: 本文原题“聊聊TCP连接耗时的那些事儿”,本次收录已征得作者同意,转载请联系作者。有少许改动。1、系列文章引言1.1 文章目的作为即时通讯技术的开发者来说,高性能、高并发相关的技术概念早就了然与胸,什么线程池、零拷贝、多路复用、事件驱动、epoll等等名词信手拈来,又或许你对具有这些技术特征的技术框架比如:Java的Netty、Php的workman、Go的nget等熟练掌...  阅读全文

posted @ 2020-12-23 16:19 Jack Jiang 阅读(196) | 评论 (0)编辑 收藏

2020年12月18日

本文由淘宝消息业务团队李历岷(花名骨来)原创分享,首次发表于公众号“淘系技术”,有修订和改动。

1、引言

本文来自淘宝消息业务团队的技术实践分享,分析了电商IM消息平台在非传统IM应用场景下的高发并、强互动群聊和直播业务中的技术特点,总结并分享了在这些场景下实现大量多对多实时消息分发投递的一些架构方面的设计实践。

目前,阿里的IM消息业务团队负责新零售领域 IM 消息平台的建设,通过 IM即时通讯产品(push、聊天机器人、单聊、群聊、消息号和聊天室)构建连接消费者和商家的沟通和触达渠道,每天处理百亿级的消费规模,支撑了直播互动、客服服务、商家群运营、品牌资讯、营销推送等电商领域 BC 互通的业务场景。

本文同步发布于:http://www.52im.net/thread-3252-1-1.html

2、技术背景

2020年双11,第一次改变节奏,从光棍节变成双节棍,从一个峰变成了两个峰,在新的挑战下,如何做好技术保障,做好技术支撑,所有技术人都进入了一个新的学习过程。

新的形势下,显著特点是时间跨度长、活动周期长以及用户互动玩法更多。从单用户CC分享到群内分享,以及直播间互动消息等,作为电商的IM消息平台,承接着整个互动的场地。

用户在整个“互动消息”场景下,可以进行实时分享、聊天、客服沟通、商家优惠、商家优惠活动和红包以及商品秒杀等。

“消息”作为用户和用户之间架起“连接”的重要桥梁,消息连接了用户和用户、用户和商家、用户和平台等。

如何做到高并发强互动下消息有序地呈现在用户面前,是消息团队亘古不变的主题,尤其是在用户的互动行为不确定性情况下,在面对不确定性的流量和规模时,系统应该怎样做到“丝般顺滑”的体验呢?本文将带着读者一起来进行深入探讨。

3、强互动消息场景的技术挑战

不同于传统社区IM消息平台,电商IM消息平台有自已的互动场景特点。

3.1 直播间

淘宝直播带来新的流量变化,百万在线已经成为常态化,大量的直播间7X24小时直播带货,用户在直播频道页频繁地进出不同的直播间。

有时候被某个直播间吸引,停在那里等着优惠、红包和宝贝,随着主播在直播间喊一嗓子,推送一张宝贝卡片,几十万人蜂拥而至秒杀。这样的场景每天都在上演 ...

3.2 互动PK群

主互动玩法“超级星秀猫瓜分20亿红包”开启的时候,意味着大量互动拉赞、分享到好友、分享到群成为流量的入口,无论淘口令/图片等。

大量的消息卡片被转发、二次转发带来的分享裂变效应,每天09:00和晚上22:00成为活跃的峰值,每个组队的用户拉群互赞,这样的场景从10月20日到11月11日持续每天固定时段上演 ...

3.3 不确定性流量

分享面板入口一次列表排布改变,营销活动的一次优惠推送,直播频道页一次运营活动都可能导致直播间/群的流量瞬间波动。

纷涌而至的用户带来了大量的互动行为,各种点击/分享/消息发送纷至沓来。

如何应对这样的流量和规模,作为平台系统,必须能够做到应对不确定性流量能力和机制,必须从流量入口到流量出口端到端考虑整体的全链路架构,是否存在单点瓶颈和缺口。

尤其在大促前期,系统架构梳理、强弱依赖梳理、上下游链路串联、破坏性压测、脉冲流量压测和全链路压测保障和优化,以及配合相关的流量控制、预案保护、业务流量隔离机制、资源调控等手段,才得以做到“不确定性流量”向“确定性流量”转变,才可以更大程度上保障系统高可用和极致用户体验。

4、强互动群聊中的消息架构实践

4.1 传统IM中“写扩散”架构的瓶颈

随着2018年淘系电商首推“双11合伙人计划”,更简单直接的双11玩法,给大众带来更多期待和惊喜。

尤其是盖楼互赞活动更是把“群聊”推上风口浪尖, 在手淘APP内部分享比在微信和钉钉等社交/企业软件分享更加便捷和高效,用户不停地在群内互动分享拉赞、通过好友助力提升盖楼积分。

基于传统的IM架构技术,尤其在群内聊天或者分享,每条消息按照群内人数进行写扩散,按照主互动500人群规模来计算,平均群大小320+,1:N的写入必然导致写入DB的RT以及存储压力,按照DB库承接120w/s写入速度,导致消息上行3K/s的极限,而实际参与互动分享的用户在峰值的时候远大于这部分互动分享和聊天消息流量。

其次集群的写入不可能完全给IM聊天消息,还有其它的营销活动、交易、物流等通知类型的消息。

基于传统IM的“写扩散”架构,在高并发、强互动场景下遇到了瓶颈,导致消息大量的延迟下推,影响最终用户体验。

4.2 “读扩散”架构的应用

基于写扩散架构的消息扩散比是1:N,那能否做到消息扩散比是1:1呢?

答案是肯定的:针对群内的消息可以分为“非个性化消息”和“个性化消息”,所谓“非个性化消息”就是大家看到都是一样的,应该只需要写一条数据,群内成员可以共享取这条数据(所谓“个性化消息”,指定某个成员发送的单边消息,譬如进群欢迎语等)。

在群内,99.99%都是“非个性化消息”,也就是可以从1:N压缩到1:1写入。

基于这个理论假设,需要考虑“读扩散”让每个用户的消息从对应的“群会话消息队列同步“数据,而不是从”用户队列同步“数据。

其中同步队列围绕队列offset偏移量进行,通过队列的自增syncId保证有序,每个客户端维护相应的队列的同步位点,采取“客户端存储位点的去中心化“方案,实现”下行消息的推拉“结合。

通过队列位点syncId进行比对,如果服务端消息队列syncId-客户端队列syncId=1,表示云端消息无空洞,否则携带客户端的队列和对应的syncId到云端重新同步区间数据,实现最终一致性。

传统的IM产品:腾讯QQ、腾讯微信、网易云通讯、抖音IM、钉钉IM、脉脉IM、支付宝IM。

PS:市面上APP80%都具备IM聊天能力,均采取写扩散简单模式进行云端消息同步。

4.3 “读扩散”、“写扩散”技术方案对比

4.3.1)写扩散技术:

优点:

  • 1)整体架构简洁,方案简单,维护用户同步队列实现数据同步机制;
  • 2)无论是单聊还是群聊等会话消息,写入用户维度同步队列,集中获取同步数据;
  • 3)推和拉情况下,存储模型和数据处理简单,且天然支持个性化数据。

缺点:

  • 1)群会话消息,天然存在1:N写入扩散比,存储压力N倍压力,在线用户收到消息延迟增大;
  • 2)多个群内消息队列混合在同步队列,无优先级处理能力,无法针对互动群等做隔离。

4.3.2)读扩散技术:

优点:

  • 1)降低写扩散N倍的DB存储压力,减少下行在线用户端到端扩散的RT时间;
  • 2)提升消息上行集群整体的吞吐量,用户体验更流畅;
  • 3)端到端实现会话级别的同步优先级队列,实现差异化服务。

缺点:

  • 1)整体架构偏复杂,需要维护每个动态会话消息同步队列,端侧需要实时感知新增的动态同步队列;
  • 2)客户端冷启动需要分散读取多个会话消息同步队列数据,对于存储会带来读QPS压力。

4.4 读写扩散混合模式

核心在于架构的演进推进“读写扩散混合模式“落地,结合两者的优点进行云端一体化数据同步技术,覆盖几千万互动群用户,保证整体峰值上行消息以及用户在群内端到端延迟体验,做到一条上行消息500ms以内到达群内其他用户端侧,让整体用户体验提升明显,群内强互动成为可能。

5、电商直播互动中的消息架构实践

5.1 技术挑战

电商直播呈现大直播若干个(大于30W同时在线)、中直播间几百个、小直播几万个这样分布,尤其是晚会和主播带来的热点直播间效应,对系统整体的IM消息架构存在热点带来严重挑战。

热点问题的产生需要从不确定性的流量来源说起。

直播间人员规模天然和群聊不一样(单个群<=500人),一个直播间可以突破40万-200万之间设备同时在线。

直播间在另一个层面是“特殊的群”,群维护了群和群成员强关系,直播间维护直播和设备之间的订阅弱关系,在扩散维度群按照500人进行分库分表切片模式分发下行消息,直播间需要以直播间几十万设备作为分段切片进行分发。同时直播间的指令消息如果不加以干预,会形成超大的流量热点和扩散问题。那么针对这个问题如何应对?

直播间互动全链路和核心处理节点简易时序图如下:

 

即:观众互动消息 -> 网关接入 -> 应用系统Buffer(秒级直播间维度消息) -> 编码、合并、批量消息流 -> 直播间维度设备扩散 -> 设备长连通道推送 -> 设备接收 -> 解码消息 -> 业务处理。

5.2 应对手段

5.2.1)充分利用直播间在线人数:

针对大直播间和小直播间区分对待,充分利用在线人数这个关键性指标。

譬如小直播间不做buffer队列处理(所谓buffer,就是维护topic维度的缓冲队列),支持N秒或消息条数的异步flush机制,实现削峰过程。

其中针对“可靠消息”进行持久化缓存存储,如果当前消息推送失败,重试3次或者下一条消息再次携带失败的可靠消息,整体按照推拉结合的方式,端侧做重复消息去重控制。

5.2.2)用户进出房间关系维护:

从用户第一次进直播间,发起订阅请求,最终通过大小直播间分片进行路由到指定的直播间,针对特殊大直播间提前做好集群分片。

或者通过每天的离线数据分析大直播间的账号,主播开播创建直播间的时候,提前做好分片确定性,在脉冲绑定关系的时候,流量分散到N台订阅集群进行绑定关系建立。

5.2.3)流控策略考虑:

流控不等于一刀切,而是针对不同的subType指令进行控制。

针对可靠消息(红包、优惠、宝贝卡片)等进行持久化存储,利用多次消息下推携带机制,同时兼顾端侧拉取机制,保证消息最终一致性且在3秒以内到达端侧。

5.2.4)一致性hash问题-热点直播间:

一致性Hash的架构必然存在热点问题,如果在直播间进行分散架构,必然存在指令风暴问题。基于此,需要进行Hash热点二次打散处理。

针对这样的热点问题技术上是可行的,热点问题散列到多台IP机器上进行流量承接,另外需要考虑直播间维度的统计数据分布式合并等问题,需要用到分布式锁并发写入统计数据,且直播维度数据合并计算,类似JDKStriped64原理。

6、“群聊”和“直播互动”的消息架构差异思考

因为早期两套系统各自演进应对不同的流量和产品需求,用户规模化和产品要求带来的差异化架构。

对比“群聊”和“直播间互动”两类互动场景,群聊基于消息、会话、会话视图以及附加消息存储和消息漫游能力进行整体设计。而对于直播间来说,围绕直播间大规模实时互动进行抽象设计,充分实现buffer/批量/订阅关系切片等机制。

对于关系来说:群关系是强关系,需要用户确认加群,才可以进群、退群、查看群好友等。对于直播间来说是弱关系,用户进直播、退出直播都是自动完成关系建立和取消,当然用户可以后续进入回放直播间。

因此:在功能上和模型设计上需要进一步思考,尤其现有回放直播间需要一套录制/回放指令机制,保存互动指令等关键性指令数据,而这点在消息IM场景完全支持了,包括用户可以漫游历史消息等。

技术的发展不会一尘不变,针对合适的场景进行差异化架构和设计才是最重要的,同样在应对不确定性流量这个场景下,解决问题的方式是不完全相同的,这个过程需要反复迭代和优化,围绕端到端用户体验这个核心目标进行技术演进才是最重要的价值选择和方向判断。

7、不确定性流量的消息QoS思考

不确定性的流量如何转为确定性流量,是技术必须要解决的“确定性”问题。

尤其面对流量需要确定性进行分解,分优先级保障不同的消息QoS能力是“高可用架构”永恒不变的主题,就像应用系统的限流,需要分场景进行流控一样。

基于这样的思考推演,QoS分级机制来承诺消息服务SLA,最终才可以做到隔离/优先级/差异化处理,完成整体的消息顺滑体验。

 

8、写在最后

面对不确定性的流量,需要进行流量再细分以及面向不同的场景采取不同的处理方案去应对,惯用的手段是指令合并处理、链路隔离和共享、租户存储隔离、流量打标机制区分场景来源等,以及有相对确定性的流量大脑进行观测,在峰值流量来临之前,需要具备流量感知和预警机制,做好流量的优先级划分,应急预案是快速失败限流还是慢速等待消费以及优先级控制等机制。

同时在系统层面,需要做到水平扩展能力,也就是能否通过弹性扩容的方式应对高流量的峰值,另外需要做到整体全链路合理的架构设计,避免“写入放大”以及“读取放大”的单点问题产生,需要针对入口流量和出口流量进行比对,是否可以做到上下游1:1的情况下,尽可能地避免单点瓶颈带来集群瓶颈乃至整体不可用。

“高可用架构“是技术人员永恒不变的主题之一,随着用户规模增长以及集中流量爆发的情形下,更需要敏锐地找到全链路单点问题,提前预判定义出面临的问题。然后给出合理的优化方案,最终灰度切流以及全链路压测验证保证整体方案有序推进,尤其面对的在线系统优化,好比开着飞机换轮胎一样。具备优化效果数据和监控是衡量每一步的预期收益,只有这样才可以做好“架构演进”,才可以持续交付更好的用户产品体验,赢得用户的喜爱,以及互动效果和产品流畅性才可以得到用户满意度提升。架构演进是个动态化的过程,需要持续投入和改进,推动全链路整体落地。

附录:更多相关资料

[1] 有关阿里巴巴的文章分享:

阿里钉钉技术分享:企业级IM王者——钉钉在后端架构上的过人之处

现代IM系统中聊天消息的同步和存储方案探讨

阿里技术分享:深度揭秘阿里数据库技术方案的10年变迁史

阿里技术分享:阿里自研金融级数据库OceanBase的艰辛成长之路

来自阿里OpenIM:打造安全可靠即时通讯服务的技术实践分享

钉钉——基于IM技术的新一代企业OA平台的技术挑战(视频+PPT) [附件下载]

阿里技术结晶:《阿里巴巴Java开发手册(规约)-华山版》[附件下载]

重磅发布:《阿里巴巴Android开发手册(规约)》[附件下载]

作者谈《阿里巴巴Java开发手册(规约)》背后的故事

《阿里巴巴Android开发手册(规约)》背后的故事

干了这碗鸡汤:从理发店小弟到阿里P10技术大牛

揭秘阿里、腾讯、华为、百度的职级和薪酬体系

淘宝技术分享:手淘亿级移动端接入层网关的技术演进之路

难得干货,揭秘支付宝的2维码扫码技术优化实践之路

淘宝直播技术干货:高清、低延时的实时视频直播技术解密

阿里技术分享:电商IM消息平台,在群聊、直播场景下的技术实践

[2] 有关IM架构设计的文章:

浅谈IM系统的架构设计

简述移动端IM开发的那些坑:架构设计、通信协议和客户端

一套海量在线用户的移动端IM架构设计实践分享(含详细图文)

一套原创分布式即时通讯(IM)系统理论架构方案

从零到卓越:京东客服即时通讯系统的技术架构演进历程

蘑菇街即时通讯/IM服务器开发之架构选择

腾讯QQ1.4亿在线用户的技术挑战和架构演进之路PPT

微信后台基于时间序的海量数据冷热分级架构设计实践

微信技术总监谈架构:微信之道——大道至简(演讲全文)

如何解读《微信技术总监谈架构:微信之道——大道至简》

快速裂变:见证微信强大后台架构从0到1的演进历程(一)

17年的实践:腾讯海量产品的技术方法论

移动端IM中大规模群消息的推送如何保证效率、实时性?

现代IM系统中聊天消息的同步和存储方案探讨

IM开发基础知识补课(二):如何设计大量图片文件的服务端存储架构?

IM开发基础知识补课(三):快速理解服务端数据库读写分离原理及实践建议

IM开发基础知识补课(四):正确理解HTTP短连接中的Cookie、Session和Token

WhatsApp技术实践分享:32人工程团队创造的技术神话

微信朋友圈千亿访问量背后的技术挑战和实践总结

王者荣耀2亿用户量的背后:产品定位、技术架构、网络方案等

IM系统的MQ消息中间件选型:Kafka还是RabbitMQ?

腾讯资深架构师干货总结:一文读懂大型分布式系统设计的方方面面

以微博类应用场景为例,总结海量社交系统的架构设计步骤

快速理解高性能HTTP服务端的负载均衡技术原理

子弹短信光鲜的背后:网易云信首席架构师分享亿级IM平台的技术实践

知乎技术分享:从单机到2000万QPS并发的Redis高性能缓存实践之路

IM开发基础知识补课(五):通俗易懂,正确理解并用好MQ消息队列

微信技术分享:微信的海量IM聊天消息序列号生成实践(算法原理篇)

微信技术分享:微信的海量IM聊天消息序列号生成实践(容灾方案篇)

新手入门:零基础理解大型分布式架构的演进历史、技术原理、最佳实践

一套高可用、易伸缩、高并发的IM群聊、单聊架构方案设计实践

阿里技术分享:深度揭秘阿里数据库技术方案的10年变迁史

阿里技术分享:阿里自研金融级数据库OceanBase的艰辛成长之路

社交软件红包技术解密(一):全面解密QQ红包技术方案——架构、技术实现等

社交软件红包技术解密(二):解密微信摇一摇红包从0到1的技术演进

社交软件红包技术解密(三):微信摇一摇红包雨背后的技术细节

社交软件红包技术解密(四):微信红包系统是如何应对高并发的

社交软件红包技术解密(五):微信红包系统是如何实现高可用性的

社交软件红包技术解密(六):微信红包系统的存储层架构演进实践

社交软件红包技术解密(七):支付宝红包的海量高并发技术实践

社交软件红包技术解密(八):全面解密微博红包技术方案

社交软件红包技术解密(九):谈谈手Q红包的功能逻辑、容灾、运维、架构等

社交软件红包技术解密(十):手Q客户端针对2020年春节红包的技术实践

社交软件红包技术解密(十一):解密微信红包随机算法(含代码实现)

即时通讯新手入门:一文读懂什么是Nginx?它能否实现IM的负载均衡?

即时通讯新手入门:快速理解RPC技术——基本概念、原理和用途

多维度对比5款主流分布式MQ消息队列,妈妈再也不担心我的技术选型了

从游击队到正规军(一):马蜂窝旅游网的IM系统架构演进之路

从游击队到正规军(二):马蜂窝旅游网的IM客户端架构演进和实践总结

从游击队到正规军(三):基于Go的马蜂窝旅游网分布式IM系统技术实践

IM开发基础知识补课(六):数据库用NoSQL还是SQL?读这篇就够了!

瓜子IM智能客服系统的数据架构设计(整理自现场演讲,有配套PPT)

阿里钉钉技术分享:企业级IM王者——钉钉在后端架构上的过人之处

微信后台基于时间序的新一代海量数据存储架构的设计实践

IM开发基础知识补课(九):想开发IM集群?先搞懂什么是RPC!

阿里技术分享:电商IM消息平台,在群聊、直播场景下的技术实践

>> 更多同类文章 ……

本文已同步发布于“即时通讯技术圈”公众号:

▲ 本文在公众号上的链接是:点此进入,原文链接是:http://www.52im.net/thread-3252-1-1.html

posted @ 2020-12-18 22:04 Jack Jiang 阅读(122) | 评论 (0)编辑 收藏

2020年12月9日

     摘要: 本文作者张彦飞,原题“图解Linux网络包接收过程”,内容有少许改动。1、引言因为要对百万、千万、甚至是过亿的用户提供各种网络服务,所以在一线互联网企业里面试和晋升后端开发同学的其中一个重点要求就是要能支撑高并发,要理解性能开销,会进行性能优化。而很多时候,如果你对网络底层的理解不深的话,遇到很多线上性能瓶颈你会觉得狗拿刺猬,无从下手。这篇文章将用图解的方式,从操作系统这一...  阅读全文

posted @ 2020-12-09 15:13 Jack Jiang 阅读(140) | 评论 (0)编辑 收藏

Jack Jiang的 Mail: jb2011@163.com, 联系QQ: 413980957, 微信: hellojackjiang