爱奇艺网络协程编写高并发应用实践
Posted wenwubp
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了爱奇艺网络协程编写高并发应用实践相关的知识,希望对你有一定的参考价值。
本?以爱奇艺开源的?络协程库(https://www.jintianxuesha.com)为例,讲解?络协程的设计原理、编程实践、性能优化等??内容。
?、概述
早年间, ?持多个?户并发访问的服务应?,往往采?多进程?式,即针对每?个 TCP ?络连接创建?个服务进程。在 2000 年左右,?较流?使? CGI ?式编写 Web 服务,当时?们?的?较多的 Web 服务器是基于多进程模式开发的 Apache1.3.x 系列,因为进程占?系统资源较多,所以?们开始使?多线程?式编写 Web 应用服务,线程占?的资源更少,这使单台服务器?撑的?户并发度提?了,但依然存在资源浪费的问题。因为在多进程或多线程编程?式下,均采?了阻塞通信?式,对于慢连接请求,会使服务端的进程或线程因『等待』客户端的请求数据?不能做别的事情,??浪费了操作系统的调度时间和系统资源。这种?对?的服务?式在?域?的环境下显示变得不够廉价,于是?们开始采??阻塞?络编程?式来提升服务端网络并发度,?较著名的 Web 服务器 nginx 就是?阻塞通信服务的典型代表,另外还有象 Java Netty 这样的?阻塞?络开发库。
?阻塞?络编程?直以?并发和?难度?著称,这种编程?式虽然有效的提升了服务器的利?率和处理能力,但却对??程序员提出了较?挑战,因为?阻塞 IO 的编程?式往往会把业务逻辑分隔的?离破碎,需要在通信过程中记录?量的中间状态,?且还需要处理各种异常情况,最终带来的后果就是开发周期?、复杂度?,?且难于维护。
阻塞式?络编程实现容易但并发度不?,?阻塞?络编程并发度?但编写难,针对这两种?络编程?式的优缺点,?们提出了使?协程?式编写?络程序的思想。其实协程本身并不是?个新概念,早在2000年前Windows NT 上就出现了『纤程』的 API,号称可以创建成千上万个纤程来处理业务,在 BSD Unix 上可以?来实现协程切换的 API <ucontext.h> 在 2002 年就已经存在了,当然另外?于上下?跳转的 API<setjmp.h> 出现的更早(1993年)。虽然协程的概念出现的较早,但?们终不能发现其广泛的应?场景,象『longjmp』这些 API 多?在?些异常跳转上,如 Postfix(著名的邮件MTA)在处理?络异常时?其实现程序跳转。直到 Russ Cox 在 Go 语?中加?了协程(Goroutine)的功能,使?协程进??并发?络编程才变得的简单易?。
Russ Cox 早在 2002 年就编写了?个简单的?络协程库 libtask(https://swtch.com/libtask/ ),代码量不多,却可以使我们?较清晰地看到『通过使?络 IO 协程化,使编写?并发?络程序变得如此简单』。
?、?络协程基本原理
?络协程的本质是将应?层的阻塞式 IO 过程在底层转换成?阻塞 IO 过程,并通过程序运?栈的上下?切换使 IO 准备就绪的协程交替运?,从?达到以简单?式编写?并发?络程序的?的。既然?络协程的底层也是?阻塞IO过程,所以在介绍?络协程基本原理前,我们先了解?下?阻塞?络通信的基本过程。
2.1、?络?阻塞编程
下?给出了?阻塞?络编程的常?设计?式:
• 使?操作系统提供的多路复?事件引擎 API(select/poll/epoll/kqueue etc),将?络套接字的?络读写事件注册到事件引擎中;
• 当套接字满?可读或可写条件时,事件引擎设置套接字对应的事件状态并返回给调?者;
• 调?者根据套接字的事件状态分别『回调』对应的处理过程;
• 对于?部分基于 TCP 的?络应?,数据的读写往往不是?次 IO 就能完成的,因此,一次会话过程就会有多次 IO 读写过程,在每次 IO 过程中都需要缓存读写的数据,直?本次数据会话完成。
下图以?阻塞读为例展示了整个异步?阻塞读及回调处理过程:
相对于阻塞式读的处理过程,?阻塞过程要复杂很多:
• ?次完整的 IO 会话过程会被分割成多次的 IO 过程;
• 每次 IO 过程需要缓存部分数据及当前会话的处理状态;
• 要求解析器(如:Json/Xml/Mime 解析器)最好能?持流式解析?式,否则就需要读到完整数据后才能交给解析器去处理,当遇到业数据较?时就需要分配较?的连续内存块,必然会造成系统的内存分配压?;
• 当前?部分后台系统(如数据库、存储系统、缓存系统)所提供的客户端驱动都是阻塞式的,?法直接应?在?阻塞通信应?中,从?限制了?阻塞通信?式的应?范围;
• 多次 IO 过程将应?的业务处理逻辑分割的?离破碎,??增加了业务编写过程的复杂度,降低了开发效率,同时加?了后期的不易维护性。
2.2、?络协程编程
(一)概念:在了解使?协程编写?络程序之前,需要先了解?个概念:
-
最?调度单元:当前?部分操作系统的最?调度单元是线程,即在单核或多核 CPU 环境中,操作系统是以线程为基本调度单元的,操作系统负责将多个线程任务唤?唤出;
-
上下?切换: 当操作系统需要将某个线程挂起时,会将该线程在 CPU 寄存器中的栈指针、状态字等保存?该线程的内存栈中;当操作系统需要唤醒某个被挂起的线程时(重新放置在CPU中运?),会将该线程之前被挂起的栈指针重新置? CPU 寄存器中,并恢复之前保留的状态字等信息,从?使该线程继续运?;通过这样的挂起与唤醒操作,便完成了不同线程间的上下?切换;
-
并?与?络并发:并?是指同?『时刻』同时运?的任务数,并?任务数量取决于 CPU 核?数量;??络并发是指在某?『时刻』?络连接的数量;类似于??定律,在客户端与服务端保持 TCP ?连接时,?部分连接是空闲的,所以服务端只需响应少量活跃的?络连接即可,如果服务端采?多路复?技术,即使使?单核也可以?持 100K 个?络并发连接。
(二)协程的切换过程
既然操作系统进?任务调度的最?单元是线程,所以操作系统?法感知协程的存在,?然也就?法对其进?调度;
因此,存在于线程中的?量协程需要相互协作,合理地占? CPU 时间?,在合适的运?点(如:?络阻塞点)主动让出 CPU,给其它协程提供运?的机会,这也正是『协程』这一概念的由来。每个协程一般都会经历如下过程:
协程之间的切换?般可分为『星形切换』和『环形切换』,参照下图:
当有?量的协程需要运?时,在『环形切换』模式下,前?个协程运?完毕后直接『唤醒』并切换?下?个协程,??需象『星形切换』那样先切换?调度原点,再从调度原点来『唤醒』下?个协程;因『环形切换』?『星形切换』节省了?次上下?的切换过程,所以『环形切换』?式的切换效率更?。
(三)?络过程协程化
下图是使用网络过程协程化示意图:
在网络协程库中,内部有一个缺省的IO调度协程,其负责处理与网络IO相关的协程调度过程,故称之为IO调度协程:
-
每?个?络连接绑定?个套接字句柄,该套接字绑定?个协程;
-
当对?络套接字进?读或写发生阻塞时,将该套接字添加? IO 调度协程的事件引擎中并设置读写事件,然后将该协程挂起;这样所有处于读写等待状态的?络协程都被挂起,且与之关联的?络套接字均由 IO 调度协程的事件引擎统?监控管理;
-
当某些?络套接字满?可读或可写条件时,IO 调度协程的事件引擎返回这些套接字的状态,IO 调度协程找到与这些套接字绑定的协程对象,然后将这些协程追加至协程调度队列中,使其依次运?;
-
IO 事件协程内部本身是由系统事件引擎(如:Linux 下的 epoll 事件引擎)驱动的,其内部 IO 事件的驱动机制和上?介绍的?阻塞过程相似,当某个套接字句柄『准备就绪』时,IO 调度协程便将其所绑定的协程添加进协程调度队列中,待本次 IO 调度协程返回后,会依次运?协程调度队列?的所有协程。
(四)?络协程示例
下?给出?个使?协程?式编写的?络服务器程序(更多示例参见:https://github.com/iqiyi/libfiber/tree/master/samples ):
该?络协程服务器程序处理流程为:
-
创建?个监听协程,使其『堵』在 accept() 调?上,等待客户端连接;
-
启动协程调度器,启动新创建的监听协程及内部的 IO 调度协程;
-
监听协程每接收?个网络连接,便创建?个客户端协程去处理,然后监听协程继续等待新的网络连接;
-
客户端协程以『阻塞』?式读写?络连接数据;网络连接处理完毕,则关闭连接,协程退出。
从该例?可以看出,?络协程的处理过程都是顺序?式,?较符合?的思维习惯;我们很容易将该例?改成线程?式,处理逻辑和协程?式相似,但协程?式更加轻量、占?资源更少,并发能?更强。
简单的表?必定隐藏着复杂的底层设计,因为?络协程过程在底层还是需要转为『?阻塞』处理过程,只是使?者并未感知?已。
三、?络协程核?设计要点
在介绍了?络协程的基本原理后,本章节主要介绍 libfiber ?络协程的核?设计要点,为?络协程应?实践化提供了基本的设计思路。
3.1、协程调度
libfiber 采?了单线程调度?式,主要是为了避免设计上的复杂度及效率上的影响。
如果设计成多线程调度模式,则必须?先需要考虑如下几点:
-
多核环境下 CPU 缓存的亲和性:CPU 本身配有?效的多级缓存,虽然 CPU 多级缓存容量较内存?的多,但其访问效率却远?于内存,在单线程调度?式下,可以?便编译器有效地进? CPU 缓存使?优化,使运?指令和共享数据尽可能放置在 CPU 缓存中,?如果采?多线程调度?式,多个线程间共享的数据就可能使 CPU 缓存失效,容易造成调度线程越多,协程的运?效率越低的问题;
-
多线程分配任务时的同步问题:当多个线程需要从公共协程任务资源中获取协程任务时,需要增加『锁』保护机制,?旦产??量的『锁』冲突,则势必会造成运?性能的严重损耗;
-
事件引擎操作优化:在多线程调度则很难进?如此优化,下?会介绍在单线程调度模式下的事件引擎操作优化。
当然,设计成单线程调度也需解决如下问题:
(1)、如何有效地使?多核:
在单线程调度?式下,该线程内的多个协程在运?时仅能使?单核,解决?案为:
-
启动多个进程,每个进程运??个线程,该线程运行一个协程调度器;
-
同?进程内启动多个线程,每个线程运?独?的协程调度器;
(2)、多个线程之间的资源共享:
因为协程调度是不跨线程的,在设计协程互斥锁时需要考虑:
-
协程锁需要?持『同?线程内的协程之间、不同线程的协程之间、协程线程与?协程线程之间』的互斥;
-
?络连接池的线程隔离机制,需要为每个线程建?各?独?的连接池,防?连接对象在不同线程的协程之间共享,否则便会造成同??络连接在不同线程的协程之间使?,破坏单线程调度规则;
-
需要防?线程内的某个协程『疯狂』占? CPU 资源,导致本线程内的其它协程得不到运?的机会,虽然此类问题在多线程调度时也会造成问题,但显然在单线程调度时造成的后果更为严重。
3.2、协程事件引擎设计
3.2.1、跨平台性
libfiber 的事件引擎?持当今主流的操作系统,从?为 libfiber 的跨平台特性提供了有?的?撑,下?为 libfiber 事件引擎所?持的平台:
Linux:sekect/poll/epoll,epoll 为 Linux 内核级事件引擎,采?事件触发机制,不象 select/poll 的轮循?式,所以 epoll 在处理?并发?络连接时运?效率更?;BSD/MacOS:select/poll/kqueue,其中kqueue 为内核级事件引擎,在处理高并发连接时具有更?的性能;
Windows: select/poll/iocp/Windows 窗?消息,其中 iocp 为 Windows 平台下的内核级?效事件引擎;
libfiber ?持采?界?消息引擎做为底层的事件引擎,这样在编写 Windows 界?程序的?络模块时便可以使?协程?式了,之前?们在 Windows 平台编写界?程序的?络模块时,?般采?如下两种?式:
(1)、采??阻塞?式,?络模块与界?模块在同?线程中;
(2)、将?络模块放到独?的线程中运?,运?结果通过界?消息『传递』到界?线程中;
现在 libfiber ?持 Windows 界?消息引擎,我们就可以在界?线程中直接创建?络协程,直接进?阻塞式?络编程。
(Windows 界??络协程示例:https://github.com/iqiyi/libfiber/tree/master/samples/WinEchod )
3.2.2、运?效率
?家在谈论?络协程程序的运?效率时,往往只重视协程的切换效率,却忽视了事件引擎对于性能影响的重要性,虽然现在很?络协程库所采?的事件引擎都是内核级的,但仍需要合理使?才能发挥其最佳性能。
在使? libfiber 的早期版本编译?络协程服务程序时,虽然在 Linux 平台上也是采?了 epoll 事件引擎,但在对?络协程服务程序进?性能压测(使??系统命令 『# perf top -p pid』 观察运?状态)时,却发现 epoll_ctl API 占?了较?的 CPU,分析原因是 epoll_ctl 使?次数过多导致的:因为 epoll_ctl 内部在对套接字句柄进?添加、修改或删除事件操作时,需要先通过红?树的查找算法找到其对应的内部套接字对象(红?树的查找效率并不是O (1)的),如果 epoll_ctl 的调?次数过多必然会造成 CPU 的占?较?。
因为 TCP 数据在传输时是流式的,这就意味着数据接收者经常需要多次读操作才能获得完整的数据,反映到?络协程处理流程上,如下图所示:
仔细观察上?处理流程,可以发现在图中的标注4(唤醒协程)和标注5(挂起协程)之间的两个事件操作:标注2取消读事件 与 标注3注册读事件,再结合 标注1注册读事件,完全可以把注2和标注3处的两个事件取消,因为标注1?标注3的?标是 注册读事件。最后,通过缓存事件操作的中间状态,合并中间态的事件操作过程,使 libfiber 的 IO 处理性能提升 20% 左右。
下图给出了采? libfiber 编写的回显服务器与采?其它?络协程库编写的回显服务器的性能对?(对?单核条件下的 IO 处理能?):
在 libfiber 中之所以可以针对中间的事件操作过程进?合并处理,主要是因为 libfiber 的调度过程是单线程模式的,如果想要在多线程调度器中合并中间态的事件操作则要难很多:在多线程调度过程中,当套接字所绑定的协程因IO 可读被唤醒时,假设不取消该套接字的读事件,则该协程被某个线程『拿?』后,恰巧该套接字又收到新数据,内核会再次触发事件引擎,协程调度器被唤醒,此时协程调度器也许就不知该如何处理了。
3.3、协程同步机制
3.3.1、单?线程内部的协程互斥
对于象 libfiber 这样的采?单线程调度?案的协程库??,如果互斥加锁过程仅限于同?个调度线程内部,则实现?个协程互斥锁是?较容易的,下图为 libfiber 中单线程内部使?的协程互斥锁的处理流程图(参考源?件:fiber_lock.c):
同?线程内的协程在等待锁资源时,该协程将被挂起并被加?锁等待队列中,当加锁协程解锁后会唤醒锁等待队列中的头部协程,单线程内部的协程互斥锁正是利?了协程的挂起和唤醒机制。
3.3.2、多线程之间的协程互斥
虽然 libfiber 的协程调度器是单线程模式的,但却可以启动多个线程使每个线程运?独?的协程调度器,如果?些资源需要在多个线程中的协程间共享,则就需要有?把可以跨线程使?的协程互斥锁。将 libfiber 应?在多线程的简单场景时,直接使?系统提供的线程锁就可以解决很多问题,但线程锁当遇到如下场景时就显得?能为?:
上述显示了系统线程互斥锁在 libfiber 多线程使?场景中遇到的死锁问题:
• 线程A 中的协程A1 成功对线程锁1加锁;
• 线程B 中的协程B2 对线程锁2成功加锁;
当线程A中的协程A2 要对线程锁2加锁?阻塞时,则会使线程A的协程调度器阻塞,从?导致线程A中的所有协程因宿主线程A被操作系统挂起而停止运行,同样,线程B 也会因协程B1 阻塞在线程锁1上?被阻塞,最终造成了死锁问题。
使用系统线程锁时产?上述死锁的根本原因是单线程调度机制以及操作系统的最?调度单元是线程,系统对于协程是?感知的。因此,在 libfiber 中专?设计了可?于在线程的协程之间使?的事件互斥锁(源码参? fiber_event.c),其设计原理如下:
该可?于在线程之间的协程进?互斥的事件互斥锁的处理流程为:
• 协程B(假设其属于线程b)已经对事件锁加锁后;
• 协程A(假设其属于线程a)想对该事件锁加锁时,对原?数加锁失败后创建IO管道,将IO读管道置?该事件锁的IO读等待队列中,此时协程A被挂起;
• 当协程B 对事件锁解锁时,会?先获得协程A 的读管道,解锁后再向管道中写?消息,从?唤醒协程A;
• 协程A 被唤醒后读取管道中的消息,然后再次尝试对事件锁中的原?数加锁,如加锁成功便可以继续运?,否则会再次进?睡眠状态(有可能此事件锁?被其它协程提前抢占)。
在上述事件锁的加/解锁处理过程中,使?原?数和IO管道的好处是:
-
通过使?原?数可以使协程快速加锁空闲的事件锁,原?数在多线程或协程环境中的?为相同的,可以保证安全性;
-
当锁被占?时,该协程进入IO管道读等待状态而被挂起,这并不会影响其所属的线程调度器的正常运行;在 Linux 平台上可以使? eventfd 代替管道,其占?资源更少。
3.3.3、协程条件变量
在使?线程编程时,都知道线程条件变量的价值:在线程之间传递消息时往往需要组合线程条件变量和线程锁。因此,在 libfiber 中也设计了协程条件变量(源码? fiber_cond.c),通过组合使? libfiber 中的协程事件锁(fiber_event.c)和协程条件变量,?户便可以编写出?于在线程之间、线程与协程之间、线程内的协程之间、线程间的协程之间进?消息传递的消息队列。下图为使? libfiber 中协程条件变量时的交互过程:
这是?个典型的 ?产者-消费者 问题,通过组合使?协程条件变量和事件锁可以轻松实现。
3.3.4、协程信号量
使??络协程库编写的?络服务很容易实现?并发功能,可以接??量的客户端连接,但是后台系统(如:数据库)却未必能?持?并发,即使是?持?并的缓存系统(如 Redis),当网络连接数比较?时性能也会下降,所以协程服务模块不能将前端的并发压?传递到后端,给后台系统造成很?压?,我们需要提供?种?并发连接卸载机制,以保证后台系统可以平稳地运?,在 libfiber 中提供了协程信号量(源码?:fiber_semc.c)。
下?是使? libfiber 中的协程信号量对于后台系统的并发连接进行卸载保护的示意图:
当有?量协程需要访问后台系统时,通过协程信号量将?量的协程『挡在外?』,只允许部分协程与后端系统建?连接。
注: ?前 libfiber 的协程信号量仅?在同?线程内部,还不能跨线程使?,要想在多线程环境中使?,需在每个线程内部创建独?的协程信号量。
3.4、域名解析
?络协程既然?向?络应用场景,?然离不开域名的协程化?持,现在很多?络协程库的设计者往往忽视了这?点,有些?络协程库在使?系统 API 进?域名解析时为了防?阻塞协程调度器,将域名解析过程(即调?gethostbyname/getaddrinfo 等系统 API)扔给独?的线程去执?,当域名解析并发量较?时必然会造成很多线程资源被占?。
在 libfiber 中集成了第三? dns 源码,实现了域名解析过程的协程化,占?更低的系统资源,基本满?了?部分服务端应?系统对于域名解析的需求。
3.5、Hook 系统 API
在网络协程广泛使用前,很多?络库很早就存在了,并且?部分这些?络库都是阻塞式的,要改造这些?络库使之协程化的成本是?常巨?的,我们不可能采?协程?式将这些?络库重新实现?遍,?前?个?泛采?的?案是 Hook 与 IO 及网络相关的系统中 API,在 Unix 平台上 Hook 系统 API 相对简单,在初始化时,先加载并保留系统 API 的原始地址,然后编写?个与系统 API 函数名相同且参数也相同的函数,将这段代码与应?代码?起编译,则编译器会优先使?这些 Hooked API,下?的代码给出了在 Unix 平台上 Hook 系统 API 的简单示例:
在 libfiber 中Hook 了?部分与 IO 及?络相关的系统 API,下?列出 libfiber 所 Hook 的系统 API:
IO 相关 API
• 读 API:read/readv/recv/recvfrom/recvmsg;
• 写API:write/writev/send/sendto/sendmsg/sendfile64;
?络相关 API
• 套接字 API:socket/listen/accept/connect;
• 事件引擎 API:select/poll,epoll(epoll_create, epoll_ctl, epoll_wait);
• 域名解析 API:gethostbyname/gethostbyname_r, getaddrinfo/freeaddrinfo。
通过 Hook API ?式,libfiber 已经可以使 mysql 客户端库、?些 HTTP 通信库及 Redis 客户端库的?络通信协程化,这样在使??络协程编写服务端应?程序时,??降低了编程复杂度及改造成本。
四、爱奇艺核?业务的协程实践
4.1、CDN 核?模块使?协程
4.1.1、项目背景
为了使爱奇艺用户可以快速流畅地观看视频内容,就需要 CDN 系统尽量将数据缓存在 CDN 边缘节点,使用户就近访问,但因为边缘节点的存储容量有限、数据淘汰等原因,总会有一些数据在边缘节点不存在,当用户访问这些数据时,便需要回源软件去源站请求数据并下载到本地,在爱奇艺自建 CDN 系统中此回源软件的名字为『奇迅』,相对于一些开源的回源缓存软件(如:Squid,Apache Traffic,Nginx 等),『奇迅』需要解决以下问题:
• 合并回源:当多个用户访问同一段数据内容时,回源软件应合并相同请求,只向源站发起一个请求,一方面可以降低源站的压力,同时可以降低回源带宽;
• 断点续传:当数据回源时如果因网络或其它原因造成回源连接中断,则回源软件应能在原来数据断开位置继续下载剩余数据;
• 随机位置下载:因为很多用户喜欢跳跃式点播视频内容,为了能够在快速响应用户请求的同时节省带宽,要求回源软件能够快速从视频数据的任意位置下载、同时停止下载用户跳过的内容;
• 数据完整性:为了防止数据在传输过程中因网络、机器或软件重启等原因造成损坏,需要对已经下载的块数据和完整数据做完整性校验;
下面为爱奇艺自研缓存与回源软件『奇迅』的软件架构及特点描述:
4.1.2、软件架构
在爱奇艺的自建 CDN 系统中,作为数据回源及本地缓存的核心软件,奇迅承担了重要角色,该模块采用多线程多协程的软件架构设计,如下所示奇迅回源架构设计的特点总结如下
特性 |
www.yixingylzc.cn |
高并发 |
采用网络协程方式,www.jinfylzc.cn 支持高并发接入,同时简化程序设计 |
高性能 |
采用线程池 + 协程 + 连接池 + 内存池技术,提高业务处理性能 |
高吞吐 |
采用磁盘内存映射及零拷贝技术,提升磁盘及网络 IO 吞吐能力 |
低回源 |
合并相同请求,支持部分回源及部分缓存,大大降低回源带宽 |
开播快 |
采用流式数据读取方式,提升视频开播速度 |
可扩展 |
模块化分层设计,易于扩展新功能 |
易维护 |
采用统一服务器编程框架,易管理,好维护 |
奇迅的前后端通信模块均采用网络协程方式,分为前端连接接入层和后端下载任务层,为了有效地使用多核,前后端模块均启动多个线程(每个线程运行一个独立的协程调度器);对于前端连接接入模块,由于采用协程方式,所以:
• 支持更高的客户端并发连接;
• 允许更多慢连接的存在,而不会消耗更多秕资源;
• 更有助于客户端与奇迅之间保持长连接,提升响应性能。
对于后端下载模块,由于采用协程方式,在数据回源时允许建立更多的并发连接去多个源站下载数据,从而获得更快的下载速度;同时,为了节省带宽,奇迅采用合并回源策略,即当前端多个客户端请求同一段数据时,下载模块将会合并相同的请求,向源站发起一份数据请求,在合并回源请求过程中,因数据共享原因,必然存在如 “3.3.2、多线程之间的协程互斥”章节所提到的多个线程之间的协程同步互斥的需求,通过使用 libfiber 中的事件锁完美地解决了一这需求(其实,当初事件锁就是为了满足奇迅的这一需求而设计编写)。
4.1.3、项目成果
采用协程方式编写的回源与缓存软件『www.2275922.com』上线后,爱奇艺自建CDN视频卡顿比小于 2%,CDN 视频回源带宽小于 1%。
4.2、?性能 DNS 模块使?协程
4.2.1、项目背景
随着爱奇艺用户规模的迅速壮大,对于像 DNS 服务这样非常重要的基础设施的要求也越来越高,开源软件(如:www.iceason.net)已经远远不能满足要求,下面是项目初期对于自研 DNS 系统的基本要求:
• 高性能:要求单机 QPS 可以达到百万级以上;同时,DNS View 变化不影响 QPS;
• 高容错:支持集群部署,www.yifayuled.cn 可以做到单一节点故障而不会影响 DNS 服务质量;
• 高弹性:DNS服务节点可以按需要进行扩充与删减;网卡 IP 地址发生变化时,软件可以自动绑定新地址及关闭旧地址,保证服务连接性;
• 数据增量更新:当业务的域名解析地址发生变更时,可以快速地同步至 DNS 服务,使解析生效;
下面是爱奇艺自研 DNS 的软件架构及特点介绍:
4.2.2、软件架构
DNS 做为互联网的基础设施,在整个互联网中发挥着举足轻重的作用,爱奇艺为了满足自身业务的发展需要,自研了高性能 DNS(简称 HPDNS),该 DNS 的软件架构如下图所示:
HPDNS 服务的特点如下:
优点 | 说明 |
高性能 |
1、启用 Linux 3.0 内核的 REUSEPORT 功能提升多线程并行收发包的能力; 2、采用 Linux 3.0 内核的 recvmmsg/sendmmsg API,提升单次 IO 数据包收发能力 3、采用内存预分配策略,减少内存动态分配/释放时的“锁”冲突 4、针对 TCP 服务模式,采用网络协程框架,最大化 TCP 并发能力 |
高可用 |
1、采用RCU(Read Copy Update)方式更新视图数据及配置项,无需停止服务,且不影响性能 2、网卡 IP 地址变化自动感知(即可自动添加新 IP 或摘除老IP而不必停止服务) 3、采用 Keepalived 保证服务高可用 |
易管理 | 由 master 服务管理模块管理 DNS 进程,控制 DNS 进程的启动、停止、重读配置/数据、异常重启及异常报警等 |
4.2.3、项目成果
爱奇艺自研的高性能 DNS 的单机处理能力(非 www.gywhg.net 版本)可以达到 200 万次/秒以上;将业务域名变更后的信息同步至全网自建 DNS 节点可以在一分钟内完成。
五、总结
本文讲述了爱奇艺开源项目 libfiber 网络协程库的设计原理及核心设计要点,方便读者了解网络协程的设计原理及运行机制,做到知其然且知其所以然;还从爱奇艺自身的项目实践出发,总结了在应用网络协程编程时遇到的问题及解决方案,使读者能够更加全面地了解编写网络协程类应用的注意事项。
以上是关于爱奇艺网络协程编写高并发应用实践的主要内容,如果未能解决你的问题,请参考以下文章