Jack Jiang

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

1、引言

经历过稍有些规模的IM系统开发的同行们都有体会,要想实现大规模并发IM(比如亿级用户和数十亿日消息量这样的规模),在架构设计上需要一些额外的考虑,尤其是要解决用户高并发、服务高可用,架构和实现细节上都需要不短时间的打磨。

我在过往的工作经历里,亲手设计和实现了一套亿级用户量的IM,平台上线并经过6年多的验证,稳定性和可用性被验证完全达到预期。

这套IM系统,从上线至今已6年有余,本人也已经离职创业近2年,但当初设计和开发这套系统时积累和收获了大量的第一手实践经验和技术心得。

因此,想借本文把当时的架构设计经历记录下来,作为同行交流和参考,希望能提供一些启发,少走弯路。

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

2、系列文章

为了更好以进行内容呈现,本文拆分两了上下两篇。

本文是2篇文章中的第1篇:

一套亿级用户的IM架构技术干货(上篇):整体架构、服务拆分等》(本文)

《一套亿级用户的IM架构技术干货(下篇):可靠性、有序性、弱网优化等(稍后发布...)》

本篇主要总结和分享这套IM架构的总体设计和服务拆分等。

3、原作者

本文基于邓昀泽的“大规模并发IM服务架构设计”一文进行的扩展和修订,感谢原作者的分享。

邓昀泽:毕业于北京航空航天大学,现蓝猫微会创始人兼CEO,曾就职于美团、YY语音、微软和金山软件等公司,有十多年研发管理经验。

4、技术指标

在这套IM系统的架构上,技术上我们坚持高要求,经过数年的验证,也确实达到了设计预期。

这4大技术指标是:

具体解释就是:

  • 1)高可靠:确保不丢消息;
  • 2)高可用:任意机房或者服务器挂掉,不影响服务;
  • 3)实时性:不管用户在哪里,在线用户消息在1秒内达到(我们实际是75%消息可以做到120ms);
  • 4)有序性:确保用户消息的有序性,不会出现发送和接受的乱序。

5、架构拆分

从整体架构上来说,亿级用户量的IM架构整体上偏复杂。

传统开源的IM服务喜欢把所有服务做到1-2个服务里(Connector+Service模型),这样带来的问题比较严重。

传统开源的IM的问题主要体现在:

  • 1)服务代码复杂,难以持续开发和运维;
  • 2)单一业务逻辑出问题,可能会影响到其它逻辑,导致服务的全面不可用。

因此,我在做架构设计的时候尽量追求微服务化。即把整体架构进行分拆为子系统,然后子系统内按照业务逻辑分拆为微服务。

系统拆分如下图:

4个子系统的职责是:

  • 1)IM业务系统:服务IM相关的业务逻辑(比如好友关系、群关系、用户信息等);
  • 2)信令系统:负责用户登录,用户在线状态的维护,以及在线用户的下行推送;
  • 3)推送系统:负责消息的在线推送和离线推送;
  • 4)存储系统:负责消息和文件的存储和查询;

其中:信令系统和推送系统是基础设施,不只是可以为IM业务服务,也可以承载其它类似的业务逻辑(比如客服系统)。

在部署层面:采用存储3核心机房,信令和推送节点按需部署的方式(国内业务推荐8-10个点)。实际上我们只做了了北京3个机房,上海1个机房和香港一个机房的部署,就基本上满足了大陆+香港的业务需求。

下面将逐个介绍这4个子系统的细节方面。

6、IM业务系统

一说到IM,很多人脑海里跳出的第一个关键就是“即时通信”,技术上理所当然的联想到了socket,也就是大家成天嘴上说的:“长连接”。换句话说,很多对IM不了解或了解的不多的人,认为IM里的所有数据交互、业务往来都是通过“长连接”来实现的,这样话,对于本文章中拆分出的“IM业务系统”就有点不理解了。

实际上,早期的IM(比如20年前的QQ、MSN、ICQ),确实所有数据基本都是通过“长连接”(也就是程序员所说的“socket”)实现。

但如今,移动端为主端的IM时代,IM系统再也不是一个条“长连接”走天下。

现在,一个典型的IM系统数据往来通常拆分成两种服务:

  • 1)socket长连接服务(也就是本文中的“推送服务”);
  • 2)http短连接服务(就是最常用的http rest接口那些,也就是本文中的“IM业务系统”)。

通俗一点,也也就现在的IM系统,通常都是长、短连接配合一起实现的。 

比如论坛里很多热门技术方案都是这样来做的,比如最典型的这两篇:《IM单聊和群聊中的在线状态同步应该用“推”还是“拉”?》、《IM消息送达保证机制实现(二):保证离线消息的可靠投递》,文记里提到的“推”其实就是走的“长连接”、“拉”就上指的http短连接。

对于socket长连接服务就没什么好说,就是大家最常理解的那样。

IM业务系统详细来说,就是专注处理IM相关的业务逻辑,比如:

  • 1)维护用户数据:用户基本信息等;
  • 2)维护好友关系:好友请求、好友列表、好友信息等;
  • 3)维护群组信息:群创建、解散、成员管理等;
  • 4)提供数据:离线拉取、历史记录同步;
  • 5)其它逻辑:比如通过存储和推送系统,存储消息和发送通知;

按照微服务的原则,IM业务系统也被分拆为多个服务,比如:

  • 1)GInfo服务:群组信息维护;
  • 2)IM服务:处理1V1消息;
  • 3)GIM服务:处理群组消息。

7、信令系统

7.1 基本情况

信令系统主要职责是3部分: 

 

1)维护用户在线状态:

因为用户规模庞大,必然是多个集群,每个集群多台服务器为用户提供服务。

考虑到服务器运维的复杂性,我们要假定任何一个集群,任何一个服务器都可能会挂掉,而且在这种情况下要能够继续为用户提供服务。

在这种情况下,如果用户A给用户B发消息,我们需要知道用户B在哪个服务器上,才能把消息正确推送给用户B。用户在哪个信令服务,这个信息就是在线状态数据。

2)下行消息推送:

跟上一个职责有关,用户在线的时候,如果有其它用户给他发消息,那就最好不要走离线推送,而是走在线推送。

在线推送的最后一个环节,是把用户消息推送给用户设备,因为就需要知道用户登录到哪个服务器上。

3)业务分发:

信令服务不只可以处理IM请求,也可以处理其它类型的业务请求。为了处理不同的业务,就需要有分发能力。

具体做法是通过一个SVID(service id)来实现,不同的业务携带不同的SVID,信令服务就知道如何分发了。

用户通过登录服务把数据(比如IM消息)发送到信令系统,信令系统根据SVID转发给IM系统。不管后台有多少个业务,用户只需要一条链接到信令。

7.2 服务拆分

信令系统为了实现以上这3个职责,同时要确保我们服务可平行扩展的能力和稳定性,在实际的技术实现上,我们实际上把信令服务分拆为3个服务模块。

如下图所示: 

下面将逐个介绍这3个子服务。

7.3 Login服务

Login服务主要负责维护用户长链接:

  • 1)每个用户一条链接到Login服务,并按时间发心跳包给Login服务;
  • 2)服务定时检查用户链接状态和心跳包,比如发现2个心跳周期都没收到心跳,就认为用户掉线了(有假在线问题,有兴趣同学可回贴讨论)。

Login服务收到用户登录请求以后,验证uid/cookie,如果成功就把这个用户的登录信息发送给online。

此过程主要记录的信息包含:

  • 1)uid(用户id);
  • 2)Login服务器IP/Port;
  • 3)Route服务器的IP/Port。

如果用户发送IM消息,先发送到Login,Login转发给Route,Route根据服务的类型(SVID),发现是IM协议就发送给后端的IM服务。

Login对并发要求比较高,一般要支持TCP+UDP+Websocket几种方式,单服务可以做到10-250万之间。从服务稳定性角度触发,建议是控制VM的CPU/内存,单服务器以20-50万为合适。

Login服务器本身没有状态,任何一个Login服务断掉,用户端检测到以后重连另一个Login服务器就可以了,对整体服务可靠性基本没有影响。

7.4 Online服务

Online服务主要负责维护用户的在线信息:

  • 1)如果用户掉线,Online服务里信息就是空;
  • 2)如果用户在线,Online就能找到用户登录在哪个集群,哪个Login服务器上。

Online业务相对简单:多个Login服务器会连接到Online,定期同步用户登录和离线信息。

Online主要职责是:把用户状态信息存储在Redis集群里。因此也是无状态的,任何一个Online服务挂掉,不影响整体服务能力。

如果集群规模不大,用户规模也不大,Online服务也可以收到Login服务里去。

如果规模比较大,建议分拆出来,一方面简化Login的逻辑复杂度,同时避免写Redis的慢操作放在Login服务里。因为Login要同时处理50万以上的并发链接,不适合在循环里嵌入慢操作。

7.5 Route服务

Route服务的设计核心,是作为信令系统跟其它子系统的交互层。Route下接Login服务,可以接受用户业务信息(IM),也可以往用户推送下行消息。

多个后端业务系统可以接入到Route,按照服务类型(SVID, service id)注册。比如IM服务可以接入到Route, 注册SVID_IM。这样Login接收到SVID=SVID_IM的消息,转发给Route,Route就可以根据SVID转发给IM相关的服务。

Route简单的根据SVID做转发,不处理具体的业务逻辑,因此也是无状态的。一个信令集群可以有多个Route服务,任何服务挂了不影响整体服务能力。

8、推送系统

推送系统的核心任务:是接收到给用户发送下行消息的请求以后,去信令服务查询用户是否在线,如果在线走信令推送,如果不在线走离线推送(如iOS的APNS、华为推送、小米推送等)。

因为推送服务可能出现大规模并发蜂拥,比如大群激烈讨论的时候,会触发亿级的TPS。因此推送服务用Kafka做了削峰。

我在实际的技术实现上,将推送系统进行了如下细分: 

具体就是:

  • 1)PushProxy:接受用户的推送请求,写入Kafka;
  • 2)Kafka:缓存推送服务;
  • 3)PushServer:从Kafka获取推送请求,判断用户是否在线;
  • 4)PushWorker:真正推送给信令或者APNS,华为推送等。

这里同样,除了Kafka以外每个服务都是无状态的,因为也可以实现平行扩展和容错,任何服务挂掉不影响整体服务可用性。

9、存储系统

存储服务主要是负责消息的存储和查询,因为消息量巨大,对存储服务的并发能力和存储量要求巨大。

为了平衡性能、空间和成本,存储服务按数据的热度进行了分级和区别对待。

具体是:

  • 1)短期消息(7天):存储在Redis里;
  • 2)近期消息(1-3个月):存储在Mysql里,以备用户实时查询;
  • 3)历史信息:存储在HBase里,作为历史数据慢查询。

同时,为了应对超大群的大量消息处理,存储服务在实际的技术实现上,也做了比较细的分拆。

存储服务具体拆分如下图:

具体的业务划分就是:

  • 1)MsgProxy:负责接受IM子系统的存储请求,写入Kafka;
  • 2)MsgWriter:从Kafka获取写请求,按需写入Redis和Mysql;
  • 3)MsgReader:接受用户的消息查询请求,从Redis,Mysql或者HBase读数据;
  • 4)运维工具:主要是数据库的运维需求。

消息队列(Kafka)在这里角色比较重要,因为对于高并发请求(100万人公众号),需要通过消息队列来做削峰和并行。

在具体部署上:可能是3-4个MsgProxy,后端可以对应15个左右的MsgWriter。MsgWriter是比较慢的,需要同时操作多个数据库,还要保证操作的原子性。

10、本篇小结

本篇主要总结了这套亿级用户量IM系统的总体架构设计,为了高性能和横向扩展性,基于微信的理念将整个架构在实现上分成了4个子系统,分别是:IM业务系统、信令系统、推送系统、存储系统。

针对这4个子系统,在实际的技术应用层上,又进行了进一步的服务拆分和细化,使得整个架构伸缩性大大增强。

—— 下篇《一套亿级用户的IM架构技术干货(下篇):可靠性、有序性、弱网优化等》稍后发布,敬请期待 ——

附录:相关文章

浅谈IM系统的架构设计

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

一套亿级用户的IM架构技术干货(上篇):整体架构、服务拆分等

>> 更多同类文章 ……

 

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

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

posted @ 2021-03-15 23:07 Jack Jiang 阅读(487) | 评论 (0)编辑 收藏

     摘要: 本文由微信开发团队工程师“ kellyliang”原创发表于“微信后台团队”公众号,收录时有修订和改动。1、引言随着直播和类直播场景在微信内的增长,这些业务对临时消息(在线状态时的实时消息)通道的需求日益增长,直播聊天室组件应运而生。直播聊天室组件是一个基于房间的临时消息信道,主要提供消息收发、在线状态统计等功能。本文将回顾微信直播聊天室单房间海量用...  阅读全文

posted @ 2021-03-06 17:08 Jack Jiang 阅读(306) | 评论 (0)编辑 收藏

     摘要: 本文引用了“一文读懂什么是进程、线程、协程”一文的主要内容,感谢原作者的无私分享。1、系列文章引言1.1 文章目的作为即时通讯技术的开发者来说,高性能、高并发相关的技术概念早就了然与胸,什么线程池、零拷贝、多路复用、事件驱动、epoll等等名词信手拈来,又或许你对具有这些技术特征的技术框架比如:Java的Netty、Php的workman、Go的gnet等熟练掌握。但真正到...  阅读全文

posted @ 2021-03-03 13:02 Jack Jiang 阅读(231) | 评论 (0)编辑 收藏

本文原题“你管这破玩意儿叫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 阅读(315) | 评论 (0)编辑 收藏

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

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

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

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 阅读(280) | 评论 (0)编辑 收藏

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

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

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

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

本文原题“终于明白了,一文彻底理解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 阅读(228) | 评论 (0)编辑 收藏

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 阅读(212) | 评论 (0)编辑 收藏

仅列出标题
共51页: First 上一页 25 26 27 28 29 30 31 32 33 下一页 Last 
Jack Jiang的 Mail: jb2011@163.com, 联系QQ: 413980957, 微信: hellojackjiang