C/C++编程:爱奇艺网络协程编写高并发应用实践

1059 篇文章 286 订阅

本⽂以爱奇艺开源的⽹络协程库(https://github.com/iqiyi/libfiber )为例,讲解⽹络协程的设计原理、编程实践、性能优化等⽅⾯内容。

概述

早年间,支持多个用户并发访问的服务应用,往往采用多进程方式,即针对每一个TCP网络连接创建一个服务进程。在2000年左右,比较流行使用CGI方式编写Web服务,当时人们用的比较多的Web服务器模式开发的Apache1.3.x系列,因为进程占用系统资源比较多,所以人们开始使用多线程方式编写web应用程序,线程占用的资源更少,这样使得单台服务器支撑的用户并发度提高了,但依然存在资源浪费问题。因为在多进程或者多线程编程方式下,均采用了阻塞通信方式,对于慢连接请求,会使得服务器的进程或者线程因【等待】客户端的请求数据而不能做别的事情,白白浪费了操作系统的调度时间和系统资源。这种一对一服务服务方式在广域网的环境下显示变得不够廉价,于是人们开始采用非阻塞网络编程方式来提升服务器网络并发度,比较著名的Web服务器Nginx就是非阻塞通信服务的典型代表。

⾮阻塞⽹络编程⼀直以⾼并发和⾼难度⽽著称,这种编程⽅式虽然有效的提升了服务器的利⽤率和处理能力,但却对⼴⼤程序员提出了较⼤挑战,因为⾮阻塞 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过程,所以在介绍⽹络协程基本原理前,我们先了解⼀下⾮阻塞⽹络通信的基本过程.

⽹络⾮阻塞编程

下面给出了非阻塞网络编程的常见设计模式:

  • 使用操作系统提供的多路复用事件引擎API (select/poll/epoll/kqueue etc),将网络套接字的网络读写事件注册到事件引擎中;
  • 当套接字满足可读或者可写条件时,事件引擎设置套接字对应的事件状态并返回给调用者
  • 调用者根据套接字的事件状态分别【回调】对应的处理过程
  • 对于大部分基于TCP的网络应用,数据的读写往往不是一次IO就能完成的,因此,一次会话过程就会有多次IO读写过程,在每次IO过程中都需要缓存读写的数据,直到本次数据会话完成。
    在这里插入图片描述
    下图以⾮阻塞读为例展示了整个异步⾮阻塞读及回调处理过程:
    在这里插入图片描述
    相对于阻塞式读的处理过程,非阻塞过程要复杂得多。
  • 一次完整的IO会话过程会被分隔成多次的IO过程
  • 每次IO过程需要缓存部分数据以及当前会话的处理状态
  • 要求解析器(如:Json/Xml/Mime 解析器)最好能支持流式解析⽅式,否则就需要读到完整数据后才能交给解析器去处理,当遇到业数据较⼤时就需要分配较⼤的连续内存块,必然会造成系统的内存分配压⼒;
  • 当前⼤部分后台系统(如数据库、存储系统、缓存系统)所提供的客户端驱动都是阻塞式的,⽆法直接应⽤在⾮阻塞通信应⽤中,从⽽限制了⾮阻塞通信⽅式的应⽤范围;
  • 多次 IO 过程将应⽤的业务处理逻辑分割的⽀离破碎,⼤⼤增加了业务编写过程的复杂度,降低了开发效率,同时加⼤了后期的不易维护性。

网络协程编程

概念

在了解使⽤协程编写⽹络程序之前,需要先了解⼏个概念:

  • 最小调度单元:当前大部分操作系统的最小调度单元是线程,即在单核或者多核CPU环境中,操作系统是以线程为基本调度单元的,操作系统负责将多个线程任务唤⼊唤出;
  • 上下⽂切换: 当操作系统需要将某个线程挂起时,会将该线程在 CPU 寄存器中的栈指针、状态字等保存⾄该线程的内存栈中;当操作系统需要唤醒某个被挂起的线程时(重新放置在CPU中运⾏),会将该线程之前被挂起的栈指针重新置⼊ CPU 寄存器中,并恢复之前保留的状态字等信息,从⽽使该线程继续运⾏;通过这样的挂起与唤醒操作,便完成了不同线程间的上下⽂切换;
  • 并行与网络并发:并行是指同一【时刻】同时运行的任务数,并行任务数量取决于CPU核心数量;而网络并发是指在某一【时刻】网络连接的数量;类似于⼆⼋定律,在客户端与服务端保持 TCP ⻓连接时,⼤部分连接是空闲的,所以服务端只需响应少量活跃的⽹络连接即可,如果服务端采⽤多路复⽤技术,即使使⽤单核也可以⽀持 100K 个⽹络并发连接。

协程的切换过程

既然操作系统进行任务调度的最小单元是线程,所以操作系统无法感知协程的存在,自然也就无法对其进行调度;因此,存在于线程中的大量协程需要相互协作,合理的占用CPU时间片,在合适的运行点(比如网络阻塞点)主动让出CPU,给其他协程提供运行的机会,这也正是【协程这一概念的由来】。每个协程一般都会经历如下过程:
在这里插入图片描述
协程之间的切换⼀般可分为『星形切换』和『环形切换』,参照下图:
在这里插入图片描述
当有大量的协程需要运行时,在【环形切换】模式下,前一个协程运行完毕后直接【唤醒】并切换到下一个协程,而无需像【星形切换】那样先切换到调度原点,再从调度原点来【唤醒】下一个协程;因为『环形切换』比『星型切换』节省了⼀次上下⽂的切换过程,所以『环形切换』⽅式的切换效率更⾼。

⽹络过程协程化

在这里插入图片描述
在网络协程库中,内部由一个缺省的IO调度协程,其负责处理与网络IO相关的协程调度过程,故称为IO调度协程:

  • 每一个网络连接绑定一个套接字句柄,该套接字绑定一个协程
  • 当对网络套接字进行读或者写发生阻塞时,将该套接字添加到IO调度协程的实践引擎中并设置读写事件,然后将该协程挂起;这样出于读写等待状态的网络协程都被挂起,而且与之关联的网络套接字均由IO调度协程的事件引擎统一监控管理
  • 当某些网络套接字满足可读或者可写状态时,IO调度协程的事件引擎返回这些套接字的状态,IO调度协程找到与这些套接字绑定的协程对象,然后将这些协程追加到协程调度队列中,使其依次运行
  • IO事件协程内部本身是由系统事件引擎(如:Linux 下的 epoll 事件引擎)驱动的,其内部IO事件的驱动机制和上面介绍的非阻塞过程相似,当某个套接字句柄【准备就绪】时,IO调度协程就将其所有绑定的协程添加到进行协程调度队列中,待本次IO调度协程返回后,会依次运行协程调度队列里的所有协程。

⽹络协程示例

下⾯给出⼀个使⽤协程⽅式编写的⽹络服务器程序
在这里插入图片描述
该⽹络协程服务器程序处理流程为:

  • 创建一个监听协程,使其【堵】在accept()调用上,等待客户端连接
  • 启动协程调度器,启动新创建的监听协程以及内部的IO调度协程
  • 监听协程每接受一个网络连接,就创建一个客户端协程去处理,然后监听协程继续等待新的网络连接
  • 客户端协程以【阻塞】方式读写网络连接数据;网络连接处理完毕,则关闭连接,协程退出

从该例⼦可以看出,⽹络协程的处理过程都是顺序⽅式,⽐较符合⼈的思维习惯;我们很容易将该例⼦改成线程⽅式,处理逻辑和协程⽅式相似,但协程⽅式更加轻量、占⽤资源更少,并发能⼒更强。

简单的表⾯必定隐藏着复杂的底层设计,因为⽹络协程过程在底层还是需要转为『⾮阻塞』处理过程,只是使⽤者并未感知⽽已。

⽹络协程核⼼设计要点

在介绍了⽹络协程的基本原理后,本章节主要介绍 libfiber ⽹络协程的核⼼设计要点,为⽹络协程应⽤实践化提供了基本的设计思路。

协程调度

libfiber采用了单线程调度方式,主要是为了避免设计上的复杂度和效率上的影响。

如果设计成多线程调度模式,则必须首先考虑如下几点:

  • 多核环境下的CPU缓存的亲和性:CPU本身配有高效的多级缓存,虽然CPU多级缓存容量较内存小得多,但其访问效率远远高于内存,在单线程调度方式下,可以方便编译器有效的进行CPU缓存使用优化,使运行指令和共享数据尽可能放置在CPU缓存中;而如果采用多线程调度方式,多个线程间共享的数据就可能使CPU缓存失效,容易造成调度线程越多,协程的运行效率越低的问题
  • 多线程分配任务时的同步任务:当多个线程需要从公共协程任务资源中获取协程任务时,需要增加【锁】保护机制,一旦产生大量的【锁】冲突,则势必会造成运行性能的严重损耗
  • 事件引擎操作优化:在多线程调度则很难进⾏如此优化,下⾯会介绍在单线程调度模式下的事件引擎操作优化。

当然,设计成单线程调度也需要解决如下问题:

  • 如何有效的使用多核:在单线程调度下,该线程内的多个协程在运行时仅能使用单核,解决方案为:
    • 启动多个进程,每个进程运行一个线程,该线程运行一个协程调度器
    • 同一进程内启动多个线程,每个线程运行独立的协程调度器
  • 多个线程之间的资源共享:因为协程调度是不跨线程的,在设计协程互斥锁时需要考虑:
    • 协程锁需要支持【同一线程的协程之间、不同线程的协程之间、协程线程和非协程线程之间】的互斥
    • 网络连接池的线程隔离机制,需要为每个线程建立各自独立的连接池,防止连接对象在不同线程的协程之间共享,否则会造成同一网络在不同线程的协程之间使用,破坏单线程调度规则
    • 需要防止线程内的某个协程『疯狂』占用CPU资源,导致本线程内的其他协程得不到运行的机会,然后此类问题的多线程调度时也会造成问题,但显然在单线程调度时造成的后果更为严重。

协程事件引擎设计

跨平台性

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 平台编写界⾯程序的⽹络模块时,⼀般采⽤如下两种⽅式:
      • 采⽤⾮阻塞⽅式,⽹络模块与界⾯模块在同⼀线程中;
      • 将⽹络模块放到独⽴的线程中运⾏,运⾏结果通过界⾯消息『传递』到界⾯线程中;
    • 现在 libfiber ⽀持 Windows 界⾯消息引擎,我们就可以在界⾯线程中直接创建⽹络协程,直接进⾏阻塞式⽹络编程。

运行效率

⼤家在谈论⽹络协程程序的运⾏效率时,往往只重视协程的切换效率,却忽视了事件引擎对于性能影响的重要性,虽然现在基本上⽹络协程库所采⽤的事件引擎都是内核级的,但仍需要合理使⽤才能发挥其最佳性能

在使⽤ 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可读被唤醒时,假设不取消该套接字的读事件,则该协程被某个线程『拿⾛』后,恰巧该套接字又收到新数据,内核会再次触发事件引擎,协程调度器被唤醒,此时协程调度器也许就不知该如何处理了。

协程同步机制

单一线程内部的协程互斥

对于象 libfiber 这样的采⽤单线程调度⽅案的协程库⽽⾔,如果互斥加锁过程仅限于同⼀个调度线程内部,则实现⼀个协程互斥锁是⽐较容易的,下图为 libfiber 中单线程内部使⽤的协程互斥锁的处理流程图(参考源⽂件:fiber_lock.c):
在这里插入图片描述
同一线程内的协程在等待锁资源时,该协程将被挂起并被加入锁等待队列中,当加锁协程解锁后会唤醒锁等待队列中的头部协程,单线程内部的协程互斥锁正是利用了协程的挂起和唤醒机制。

多线程之间的协程互斥

虽然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 代替管道,其占⽤资源更少。

协程条件变量

在使⽤线程编程时,都知道线程条件变量的价值:在线程之间传递消息时往往需要组合线程条件变量和线程锁。因此,在 libfiber 中也设计了协程条件变量(源码⻅ fiber_cond.c),通过组合使⽤ libfiber 中的协程事件锁(fiber_event.c)和协程条件变量,⽤户便可以编写出⽤于在线程之间、线程与协程之间、线程内的协程之间、线程间的协程之间进⾏消息传递的消息队列。下图为使⽤ libfiber 中协程条件变量时的交互过程:
在这里插入图片描述
这是⼀个典型的 ⽣产者-消费者 问题,通过组合使⽤协程条件变量和事件锁可以轻松实现。

协程信号量

使⽤⽹络协程库编写的⽹络服务很容易实现⾼并发功能,可以接⼊⼤量的客户端连接,但是后台系统(如:数据库)却未必能⽀持⾼并发,即使是⽀持⾼并的缓存系统(如 Redis),当网络连接数比较⾼时性能也会下降,所以协程服务模块不能将前端的并发压⼒传递到后端,给后台系统造成很⼤压⼒,我们需要提供⼀种⾼并发连接卸载机制,以保证后台系统可以平稳地运⾏,在 libfiber 中提供了协程信号量(源码⻅:fiber_semc.c)。

下⾯是使⽤ libfiber 中的协程信号量对于后台系统的并发连接进行卸载保护的示意图:
在这里插入图片描述
当有大量协程需要访问后台系统时,通过协程信号量将大量的协程【挡在外面】,只允许部分协程与后端系统建立连接。

注:⽬前 libfiber 的协程信号量仅⽤在同⼀线程内部,还不能跨线程使⽤,要想在多线程环境中使⽤,需在每个线程内部创建独⽴的协程信号量。

域名解析

网络协程既然面向网络应用场景,自然离不开域名的协程化支持,现在很多网络协程库的设计者往往忽略了这一点,有些网络协程库在使用系统API进行域名解析时为了防止阻塞协程调度器,将域名解析过程(即调⽤gethostbyname/getaddrinfo 等系统 API)扔给独立的线程去支持,当域名解析并发量较大时必然会造成很多线程资源被占用。

在libfiber中集成了第三方的dns源码,实现了域名解析过程的协程化,占用更低的系统资源,基本满⾜了⼤部分服务端应⽤系统对于域名解析的需求。

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 客户端库的⽹络通信协程化,这样在使⽤⽹络协程编写服务端应⽤程序时,⼤⼤降低了编程复杂度及改造成本。

爱奇艺核⼼业务的协程实践

CDN 核⼼模块使⽤协程

项目背景

为了使爱奇艺用户可以快速流畅地观看视频内容,就需要 CDN 系统尽量将数据缓存在 CDN 边缘节点,使用户就近访问,但因为边缘节点的存储容量有限、数据淘汰等原因,总会有一些数据在边缘节点不存在,当用户访问这些数据时,便需要回源软件去源站请求数据并下载到本地,在爱奇艺自建 CDN 系统中此回源软件的名字为『奇迅』,相对于一些开源的回源缓存软件(如:Squid,Apache Traffic,Nginx 等),『奇迅』需要解决以下问题:

  • 合并回源:当多个用户访问同一段数据内容时,回源软件应合并相同请求,只向源站发起一个请求,一方面可以降低源站的压力,同时可以降低回源带宽;
  • 断点续传:当数据回源时如果因网络或其它原因造成回源连接中断,则回源软件应能在原来数据断开位置继续下载剩余数据;
  • 随机位置下载:因为很多用户喜欢跳跃式点播视频内容,为了能够在快速响应用户请求的同时节省带宽,要求回源软件能够快速从视频数据的任意位置下载、同时停止下载用户跳过的内容;
  • 数据完整性:为了防止数据在传输过程中因网络、机器或软件重启等原因造成损坏,需要对已经下载的块数据和完整数据做完整性校验;

下面为爱奇艺自研缓存与回源软件『奇迅』的软件架构及特点描述:

软件架构

在爱奇艺的自建 CDN 系统中,作为数据回源及本地缓存的核心软件,奇迅承担了重要角色,该模块采用多线程多协程的软件架构设计,如下所示奇迅回源架构设计的特点总结如下:

特性说明:

  • 高并发采用网络协程方式,支持高并发接入,同时简化程序设计高性能采用线程池 + 协程 + 连接池 + 内存池技术
  • 提高业务处理性能高吞吐采用磁盘内存映射及零拷贝技术,提升磁盘及网络 IO 吞吐能力低回源合并相同请求,支持部分回源及部分缓存,大大降低回源带宽开播快采用流式数据读取方式
  • 提升视频开播速度可扩展模块化分层设计,易于扩展新功能易维护采用统一服务器编程框架,易管理,好维护

奇迅的前后端通信模块均采用网络协程方式,分为前端连接接入层和后端下载任务层,为了有效地使用多核,前后端模块均启动多个线程(每个线程运行一个独立的协程调度器);

对于前端连接接入模块,由于采用协程方式,所以:

  • 支持更高的客户端并发连接;
  • 允许更多慢连接的存在,而不会消耗更多秕资源;
  • 更有助于客户端与奇迅之间保持长连接,提升响应性能。

对于后端下载模块,由于采用协程方式,所以

  • 在数据回源时允许建立更多的并发连接去多个源站下载数据,从而获得更快的下载速度
  • 同时,为了节省带宽,奇迅采用合并回源策略,即当前端多个客户端请求同一段数据时,下载模块将会合并相同的请求,向源站发起一份数据请求,
  • 在合并回源请求过程中,因数据共享原因,必然存在如 “多线程之间的协程互斥”章节所提到的多个线程之间的协程同步互斥的需求,通过使用 libfiber 中的事件锁完美地解决了一这需求(其实,当初事件锁就是为了满足奇迅的这一需求而设计编写)。

项目成果

采用协程方式编写的回源与缓存软件『奇迅』上线后,爱奇艺自建CDN视频卡顿比小于 2%,CDN 视频回源带宽小于 1%。

⾼性能 DNS 模块使⽤协程

项目背景

随着爱奇艺用户规模的迅速壮大,对于像 DNS 服务这样非常重要的基础设施的要求也越来越高,开源软件(如:Bind)已经远远不能满足要求,下面是项目初期对于自研 DNS 系统的基本要求:

  • 高性能:要求单机 QPS 可以达到百万级以上;同时,DNS View 变化不影响 QPS;
  • 高容错:支持集群部署,可以做到单一节点故障而不会影响 DNS 服务质量;
  • 高弹性:DNS服务节点可以按需要进行扩充与删减;网卡 IP 地址发生变化时,软件可以自动绑定新地址及关闭旧地址,保证服务连接性;
  • 数据增量更新:当业务的域名解析地址发生变更时,可以快速地同步至 DNS 服务,使解析生效;

下面是爱奇艺自研 DNS 的软件架构及特点介绍:

软件架构

在这里插入图片描述
HPDNS 服务的特点如下:
在这里插入图片描述

项目成果

爱奇艺自研的高性能 DNS 的单机处理能力(非 DPDK 版本)可以达到 200 万次/秒以上;将业务域名变更后的信息同步至全网自建 DNS 节点可以在一分钟内完成。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值