一、目录
- 服务端常用技术栈
- 服务端开发常用组件
- 信息流服务架构设计
- Nginx详解
二、学习内容
数据
存储
- 用户资料
- 商品
- 文章
- 图片
- 视频
传输
- 消息传递
- 转账
- 上传
- 下载
计算
数据计算
- 地图导航
- 搜索
- 信息流推荐
提供算力
- 语音合成
- 语音实时转写
- 图像内容识别
技术栈
主要包括基础设施,支撑系统,接入层,逻辑层和存储层,这是 C/S 架构模式
其中基础设施主要是服务运行的环境,大多和硬件,底层系统有关,比较泛化。而支撑系统,主要目的是支撑服务端连续运行,监控错误和漏洞,并收集服务端产生的信息,达到方便维护或者自动维护的效果
CI/CD
CI/CD 是一种通过在应用开发阶段引入自动化来频繁向客户交付应用的方法。CI/CD 的核心概念是持续集成、持续交付和持续部署。作为一个面向开发和运营团队的解决方案,CI/CD 主要针对在集成新代码时所引发的问题(亦称:“集成地狱”)。
具体而言,CI/CD 在整个应用生命周期内(从集成和测试阶段,到交付和部署)引入了持续自动化和持续监控。这些关联的事务通常被统称为 “CI/CD 管道”,由开发和运维团队以敏捷方式协同支持。
接入层
功能:
- 维护与客户端之间的网络连接,管理客户端的网络状态。
- 接收客户端请求,将请求转发到业务层,转发业务层发给客户端的数据。
- 就近接入,负载均衡,优化网络体验。
反向代理
反向代理是代理服务器的一种。服务器根据客户端的请求,从其关系的一组或多组后端服务器(如 Web 服务器)上获取资源,然后再将这些资源返回给客户端,客户端只会得知反向代理的 IP 地址,而不知道在代理服务器后面的服务器簇的存在。而 Nginx 就是性能非常好的反向代理服务器,它可以用来做负载均衡。
负载均衡
负载均衡(Load Balance)其意思就是分摊到多个操作单元上进行执行,例如 Web 服务器、FTP 服务器、企业关键应用服务器和其它关键任务服务器等,从而共同完成工作任务。
负载均衡是高可用网络基础架构的的一个关键组成部分,有了负载均衡,我们通常可以将我们的应用服务器部署多台,然后通过负载均衡将用户的请求分发到不同的服务器用来提高网站、应用、数据库或其他服务的性能以及可靠性。
负载均衡算法 :
- 轮询:为第一个请求选择健康池中的第一个后端服务器,然后按顺序往后依次选择,直到最后一个,然后循环。
- 最小连接:优先选择连接数最少,也就是压力最小的后端服务器,在会话较长的情况下可以考虑采取这种方式。
- 散列:根据请求源的 IP 的散列(hash)来选择要转发的服务器。这种方式可以一定程度上保证特定用户能连接到相同的服务器。如果你的应用需要处理状态而要求用户能连接到和之前相同的服务器,可以考虑采取这种方式。
流量清洗
对进入的数据流量进行实时监控,及时发现包括 DOS 攻击在内的异常流量。在不影响正常业务的前提下,清洗掉异常流量。
逻辑层
作用是根据业务逻辑,处理用户请求,得到结果。设计模式有单体式服务和微服务。
单体式模式
所有的业务逻辑都集中在一个代码仓库,一个进程里
优点是:简单,适合协作人数少,功能较少,且维护和更新较方便
缺点是:难以支持高复杂度的系统
微服务
业务逻辑按功能或类型作划分,分别部署多个服务,使用网络通信交换数据
优点是:多人团队协作成本更低,功能复用,系统解耦,故障隔离
缺点是:影响性能,系统复杂性大增,对业务流程设计提出了更高的要求,且对网络,基础框架的要求更高
RPC
RPC(Remote Procedure Call)— 远程过程调用,它是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议。RPC 协议假定某些传输协议的存在,如 TCP 或 UDP,为通信程序之间携带信息数据。在 OSI 网络通信模型中,RPC 跨越了传输层和应用层。RPC 使得开发包括网络分布式多程序在内的应用程序更加容易。
常用的RPC框架:
- gRPC
- bRPC
- Thrift
RPC要解决的问题是:
- 解决分布式系统中,服务之间的调用问题。
- 远程调用时,要能够像本地调用一样方便,让调用者感知不到远程调用的逻辑。
实际情况下,RPC 很少用到 http 协议来进行数据传输。而不管使用何种协议进行数据传输,一个完整的 RPC 过程,都可以用下面这张图来描述:

通信协议
HTTP:超文本传输协议,数据交换协议的一种
- 万维网数据通信的基础
- 基于 TCP,协议简单
HTTPS:TLS 加持的 HTTP
- 提供网站服务器身份认证
- 保护数据交换的隐私以及完整性校验
存储层
存储层就是 DBMS, 负责管理对数据库数据的读写和转发。DBMS 必须能迅速执行大量数据的更新和检索。现在的主流是关系数据库管理系统 (RDBMS)。因此,一般从逻辑层传送到存储层的要求大都使用 SQL 语言。
消息队列
“消息队列” 是在消息的传输过程中保存消息的容器,其中的 “消息” 指的是在两台计算机间传送的数据单位。消息可以非常简单,例如只包含文本字符串;也可以更复杂,可能包含嵌入对象。消息被发送到队列中。
消息队列管理器在将消息从它的源中继到它的目标时充当中间人。队列的主要目的是提供路由并保证消息的传递;如果发送消息时接收者不可用,消息队列会保留消息,直到可以成功地传递它。
其目的是解耦业务逻辑,削峰限流,主要特点是异步处理。
常用的有:
- Kafka:性能很高,消息理论上不会丢
- RabbitMQ:支持AMQP(Advanced Message Queuing Protocol)
- ZeroMQ:超轻量的事件组件
KV
KV(Key-Value)指的是一个解决方案是使用键值(Key-Value)存储数据库,这是一种 NoSQL(非关系型数据库)模型,其数据按照键值对的形式进行组织、索引和存储。KV 存储非常适合不涉及过多数据关系业务关系的业务数据,同时能有效减少读写磁盘的次数,比 SQL 数据库存储拥有更好的读写性能。优点是简单,性能高,横向拓展性高。
常用的有:
- Redis:性能高,支持多种数据结构,kv / list / set 等等,有一定的持久化能力
- Memcached:性能高,简单易用,作为cache使用
- MongoDB:查询功能强大,性能好
关系型数据库
支持复杂的数据关系存储和查询
常用的是 MySQL:性能卓越,服务稳定,开源
文件系统
分布式文件系统(英语:Distributed file system, DFS),或是网络文件系统(英语:Network File System),是一种允许文件通过网络在多台主机上分享的文件系统,可让多机器上的多用户分享文件和存储空间。 在这样的文件系统中,客户端并非直接访问底层的数据存储区块,而是通过网络,以特定的通信协议和服务器沟通。
常见的分布式文件系统有 GFS、HDFS、Lustre 、Ceph 、GridFS 、mogileFS、TFS、FastDFS 等。各自适用于不同的领域。它们都不是系统级的分布式文件系统,而是应用级的分布式文件存储服务。
比较:

设计信息流服务
由于我们小组所选的项目实现内容为信息流服务,因此需要着重了解如何设计一个信息流服务
需要考虑的问题有:
- 文章推荐
- 文章数据存储与读取
- 评论和点赞
整体架构如下:

符合 C/S 架构
大体流程是:
- 从推荐获取本次要出的文章列表
- 获取文章的信息
- 获取文章的评论和点赞信息
- 打包后返回给客户端
Nginx详解
Nginx 是一款自由的、开源的、高性能的 HTTP 服务器和反向代理服务器;同时也是一个 IMAP、POP3、SMTP 代理服务器;nginx 可以作为一个 HTTP 服务器进行网站的发布处理,另外 nginx 可以作为反向代理进行负载均衡的实现。
- 正向代理:客户端知道服务器端,通过代理端连接服务器端。代理端代理的是服务器端。
- 反向代理:所谓反向,是对正向而言的。服务器端知道客户端,客户端不知道服务器端,通过代理端连接服务器端。代理端代理的是客户端。代理对象刚好相反,所以叫反向代理。
Nginx的特点
- 跨平台:Nginx 可以在大多数 Unix like OS编译运行,而且也有Windows的移植版本。
- 配置异常简单,非常容易上手。配置风格跟程序开发一样,神一般的配置
- 非阻塞、高并发连接:数据复制时,磁盘I/O的第一阶段是非阻塞的。官方测试能够支撑5万并发连接,在实际生产环境中跑到2~3万并发连接数.(这得益于Nginx使用了最新的epoll模型)
- 事件驱动:通信机制采用epoll模型,支持更大的并发连接。
- master/worker结构:一个master进程,生成一个或多个worker进程
- 内存消耗小:处理大并发的请求内存消耗非常小。在3万并发连接下,开启的10个Nginx 进程才消耗150M内存(15M*10=150M)
- 成本低廉:Nginx为开源软件,可以免费使用。而购买F5 BIG-IP、NetScaler等硬件负载均衡交换机则需要十多万至几十万人民币
- 内置的健康检查功能:如果 Nginx Proxy 后端的某台 Web 服务器宕机了,不会影响前端访问。
- 节省带宽:支持 GZIP 压缩,可以添加浏览器本地缓存的 Header 头。
- 稳定性高:用于反向代理,宕机的概率微乎其微
Nginx的事件处理机制:
对于一个基本的 web 服务器来说,事件通常有三种类型,网络事件、信号、定时器。
首先,请求的基本过程:建立连接 --- 接收数据 --- 发送数据 。
其次,系统底层的操作 :上述过程(建立连接 --- 接收数据 --- 发送数据)在系统底层就是读写事件。
1. 如果采用阻塞调用的方式,当读写事件没有准备好时,必然不能够进行读写事件,那么久只好等待,等事件准备好了,才能进行读写事件。那么请求就会被耽搁 。阻塞调用会进入内核等待,cpu 就会让出去给别人用了,对单线程的 worker 来说,显然不合适,当网络事件越多时,都在等待,cpu 空闲下来没用,cpu 利用率自然上不去了,就没法高并发了。
2. 既然没有准备好阻塞调用不行,那么采用非阻塞方式。非阻塞就是,事件,马上返回 EAGAIN,告诉你,事件还没准备好,过会再来吧。过了一会,再来检查一下事件,直到事件准备好了为止,在这期间,你就可以先去做其它事情,然后再来看看事件好了没。虽然不阻塞了,但要时不时地过来检查一下事件的状态,虽然可以做更多的事情了,但带来的开销也是不小的
小结:非阻塞通过不断检查事件的状态来判断是否进行读写操作,这样带来的开销很大。
3. 因此才有了异步非阻塞的事件处理机制。具体到系统调用就是像 select/poll/epoll/kqueue 这样的系统调用。他们提供了一种机制,可以同时监控多个事件,调用他们是阻塞的,但可以设置超时时间,在超时时间之内,如果有事件准备好了,就返回。这种机制解决了上面两个问题。
以 epoll 为例:当事件没有准备好时,就放入 epoll (队列) 里面。如果有事件准备好了,那么就去处理;如果事件返回的是 EAGAIN,那么继续将其放入 epoll 里面。从而,只要有事件准备好了,我们就去处理她,只有当所有时间都没有准备好时,才在 epoll 里面等着。这样 ,就可以并发处理大量的并发了,当然,这里的并发请求,是指未处理完的请求,线程只有一个,所以同时能处理的请求当然只有一个,只是在请求间进行不断地切换而已,切换也是因为异步事件未准备好,而主动让出的。这里的切换是没有任何代价,可以理解为循环处理多个准备好的事件。
4. 与多线程的比较:
与多线程相比,这种事件处理方式是有很大的优势的,不需要创建线程,每个请求占用的内存也很少,没有上下文切换,事件处理非常的轻量级。并发数再多也不会导致无谓的资源浪费(上下文切换)。
小结:通过异步非阻塞的事件处理机制,Nginx 实现由进程循环处理多个准备好的事件,从而实现高并发和轻量级。
Nginx内部模型:

nginx 是以多进程的方式来工作的,当然 nginx 也是支持多线程的方式的,只是主流的方式还是多进程的方式,也是 nginx 的默认方式。nginx 采用多进程的方式有诸多好处。
- nginx在启动后,会有一个master进程和多个worker进程。master进程主要用来管理worker进程,包含:接收来自外界的信号,向各worker进程发送信号,监控 worker进程的运行状态,当worker进程退出后(异常情况下),会自动重新启动新的worker进程。而基本的网 络事件,则是放在worker进程中来处理了 。多个worker进程之间是对等的,他们同等竞争来自客户端的请求,各进程互相之间是独立的 。一个请求,只可能在一个worker进程中处理,一个worker进程,不可能处理其它进程的请求。 worker进程的个数是可以设置的,一般我们会设置与机器cpu核数一致,这里面的原因与nginx的进程模型以及事件处理模型是分不开的 。
- Master接收到信号以后怎样进行处理(./nginx -s reload )?首先master进程在接到信号后,会先重新加载配置文件,然后再启动新的进程,并向所有老的进程发送信号,告诉他们可以光荣退休了。新的进程在启动后,就开始接收新的请求,而老的进程在收到来自 master的信号后,就不再接收新的请求,并且在当前进程中的所有未处理完的请求处理完成后,再退出 .
- worker进程又是如何处理请求的呢?前面有提到,worker进程之间是平等的,每个进程,处理请求的机会也是一样的。当提供80端口的http服务时,一个连接请求过来,每个进程都有可能处理这个连接。首先,每个worker进程都是从master 进程fork(分配)过来,在master进程里面,先建立好需要listen的socket之后,然后再fork出多个worker进程,这样每个worker进程都可以去accept这个socket(当然不是同一个socket,只是每个进程的这个socket会监控在同一个ip地址与端口,这个在网络协议里面是允许的)。一般来说,当一个连接进来后,所有在accept在这个socket上面的进程,都会收到通知,而只有一个进程可以accept这个连接,其它的则accept失败,这是所谓的惊群现象。当然,nginx也不会视而不见,所以nginx提供了一个accept_mutex这个东西,从名字上,可以看这是一个加在accept上的一把共享锁。有了这把锁之后,同一时刻,就只会有一个进程在accpet连接,这样就不会有惊群问题了。accept_mutex是一个可控选项,我们可以显示地关掉,默认是打开的。当一个worker进程在accept这个连接之后,就开始读取请求,解析请求,处理请求,产生数据后,再返回给客户端,最后才断开连接,这样一个完整的请求就是这样的了。因此可以看到,一个请求,完全由worker进程来处理,而且只在一个worker进程中处理。
- nginx采用这种进程模型有什么好处呢?采用独立的进程,可以让互相之间不会影响,一个进程退出后,其它进程还在工作,服务不会中断,master进程则很快重新启动新的worker进程。而worker进程的异常退出,意味着程序有bug了,异常退出,会导致当前worker上的所有请求失败,不过不会影响到所有请求,所以降低了风险。
- 按理来说,nginx采用多worker的方式来处理请求,每个worker里面只有一个主线程,那能够处理的并发数很有限,多少个worker就能处理多少个并发,何来高并发呢?并非如此,nginx采用了异步非阻塞的方式来处理请求,也就是说,nginx是可以同时处理成千上万个请求的 .对于IIS服务器每个请求会独占一个工作线程,当并发数上到几千时,就同时有几千的线程在处理请求了。这对操作系统来说,是个不小的挑战,线程带来的内存占用非常大,线程的上下文切换带来的cpu开销很大,自然性能就上不去了,而这些开销完全是没有意义的。之前说过,推荐设置worker的个数为cpu的核数,在这里就很容易理解了,更多的worker数,只会导致进程来竞争cpu资源了,从而带来不必要的上下文切换。而且,nginx为了更好的利用多核特性,提供了cpu亲缘性的绑定选项,我们可以将某一个进程绑定在某一个核上,这样就不会因为进程的切换带来cache的失效。
Nginx是如何处理一个请求
首先,nginx 在启动时,会解析配置文件,得到需要监听的端口与 ip 地址,然后在 nginx 的 master 进程里面,先初始化好这个监控的 socket (创建 socket,设置 addrreuse
等选项,绑定到指定的 ip 地址端口,再 listen),然后再 fork (一个现有进程可以调用 fork 函数创建一个 新进程。由 fork 创建的新进程被称为子进程) 出多个子进程出来,然后子进程会竞争 accept 新的连接。此时,客户端就可以向 nginx 发起连接了。当客户端与 nginx 进行三次握手,与 nginx 建立好一个连接后,此时,某一个子进程会 accept 成功,得到这个建立好的连接的 socket,然后创建 nginx 对连接的封装,即 ngx_connection_t
结构体。接着,设置读写事件处理函数并添加读写事件来与客户端进行数据的交换。最后,nginx 或客户端来主动关掉连接。
当然,nginx 也是可以作为客户端来请求其它 server 的数据的(如 upstream 模块),此时,与其它 server 创建的连接,也封装在 ngx_connection_t
中。作为客户端,nginx 先获取一个 ngx_connection_t
结构体,然后创建 socket,并设置 socket 的属性( 比如非阻塞)。然后再通过添加读写事件,调用 connect/read/write
来调用连接,最后关掉连接,并释放 ngx_connection_t
。
nginx 在实现时,是通过一个连接池来管理的,每个 worker 进程都有一个独立的连接池,连接池的大小是 worker_connections
。这里的连接池里面保存的其实不是真实的连接,它只是一个 worker_connections
大小的一个 ngx_connection_t
结构的数组。并且,nginx 会通过一个链表free_connections
来保存所有的空闲 ngx_connection_t
,每次获取一个连接时,就从空闲连接链表中获取一个,用完后,再放回空闲连接链表里面。
在这里,很多人会误解 worker_connections
这个参数的意思,认为这个值就是 nginx 所能建立连接的最大值。其实不然,这个值是表示每个 worker 进程所能建立连接的最大值,所以,一个 nginx 能建立的最大连接数,应该是 worker_connections * worker_processes
。当然 ,这里说的是最大连接数,对于 HTTP 请求本地资源来说,能够支持的最大并发数量是 worker_connections * worker_processes
,而如果是 HTTP 作为反向代理来说,最大并发数量应该是 worker_connections * worker_processes/2
。因为作为反向代理服务器,每个并发会建立与客户端的连接和与后端服务的连接,会占用两个连接。