Home Authors Posts by 可乐

可乐

419 POSTS 5 COMMENTS

0 137

之前做研发的时候,因为我刚入职,需求都是由我的导师和Leader分配的,所以给到我手里时只需要我设计方案并给出工期,没有关注标准的需求研发流程。

此时,我已转岗至业务中台测试岗,在这边有标准的研发流程,我想是可以参考的,因此特意记录。

 

一、标准研发流程图

二、标准研发流程说明

1.角色关系

产品和研发的关系

  • 产品要具备独立性,独立成立产品团队/部门,独立开展工作;
  • 产品负责需求,需求决定了开发实现什么,故相关开发人员(负责相应功能模块的开发人员)一定要参加需求评审会议以便及时了解需求;
  • 开发人员最怕什么?十个有九个会说需求又变了,所以产品对于需求变更一定要慎重,通过建立起相应的需求管理规范和制度来做到无随意的需求变更,需求变更后干系人都能及时得到通知;
  • 开发人员要彻底理解需求,这是进行开发的前提;开发人员要多和产品人员沟通,及时消除对于需求的误解和疑惑;

产品和测试的关系

  • 测试人员(一般都会开展交叉测试,所以都参与)一定要参加需求评审会议以便及时了解需求;
  • 测试人员依据需求文档设计编写出测试用例后一定要进行测试用例评审并一定要邀请产品人员参会;因为产品人员对需求是最了解的。
  • 需求确认变更后测试人员要及时得到通知并尽快更新测试用例并根据实际情况是否进行测试用例评审。

测试与研发的关系

  • 测试要具备独立性,独立成立测试团队/部门,独立开展工作;
  • 测试人员要懂代码(看懂代码是基础,会写代码更好),懂代码是和开发团队的沟通利器,也是开展自动化测试的基础。当今语言很多,个人认为优先掌握Java或者Python;
  • 测试人员要有一定的沟通能力,报告缺陷时请描述清楚但去除不必要的测试步骤,也别忘了描述测试环境等相关信息,可以附带缺陷出现的截图,日志文件,甚至录制一段重现缺陷的视频都是让开发人员迅速重现缺陷的很好的办法;
  • 测试人员在报告缺陷时如有把握,可以给出解决方案,这样的测试人员我相信开发人员一定很喜欢。

2.会议类型

需求评审&需求澄清会议

产品:主持人;需要说明需求背景、需求内容、需求目标、需求完成的时间点,并通过会上讨论完善需求或者调整需求。

研发:在与会前需要大致了解需求,会上以自己对产品or业务的理解,逐一与产品、测试确认需求点,对需求不合理的地方提出质疑,可要求产品重新评估。研发人员需要更关注需求实现层面,例如实现难度、实现方式是否合理等。

测试:在与会前需要大致了解需求,会上以自己对产品or业务的理解,逐一与产品、研发确认需求点,对需求不合理的地方提出质疑,可要求产品重新评估。测试人员需要更关注需求所带来的影响,例如是否会影响线上业务、是否存在潜在风险点等。

注:会议可分三阶段:会前、会中、会后;

  1. 会前要求各方对需求有大致了解。
  2. 会中要求各方落实需求细节,并记录待修改、待确认的问题,不可有遗漏点。
  3. 会后要求各方对会议提出的问题进行跟进,产品及时更新需求Tapd文档,研发及时更新需求技术方案,测试及时调整设计用例。

需求阶段结束、进入方案阶段

需求评审&需求澄清会议

 

0 129

我,一名新入职的工程师;

我,缺乏高并发实战经验;

我,后端回参上千条数据;

我,循环访问数据库一次查一条;

我,从不分页,从不批量处理;

我被称为性能杀手,服务器终结者。

 

秋招时,我能和面试官大谈高并发,高可用,高扩展;答辩时,我能和老师探讨多线程,分布式锁,性能优化…

面试官和老师眼中的我:

查看源图像

实际上的我:

动图

 

实际工作中,为什么要分页?为什么不应该在循环中查询数据库?怎样在工作中去考虑性能问题?相信本文能带给你一些思考。


一、为什么要分页?

分页功能在网页中是非常常见的一个功能,其作用也就是将数据分割成多个页面来进行显示。

  • 使用场景: 当取到的数据量达到一定的时候,就需要使用分页来进行数据分割。

当我们不使用分页功能的时候,会面临许多的问题:

  • 客户端的问题: 如果数据量太多,都显示在同一个页面的话,会因为页面太长严重影响到用户的体验,也不便于操作,也会出现加载太慢的问题
  • 服务端的问题: 如果数据量太多,可能会造成内存溢出(OOM),而且一次请求携带的数据太多,对服务器的性能也是一个考验

 

以C/S架构为例,要实现“展示所有的商品信息”功能,实现流程是:用户点击功能按钮,前端发起请求,后端接收请求去查对应库表,返回响应给前端,前端将数据渲染到App页面上。

假使商品信息表中有上百万条记录,如果不做分页处理,后端直接从表中获取百万条记录

服务器压力过大

首先服务器的内存可能就会溢出,即OOM。

假设公司老板财大气粗,服务器内存特别大,JVM的内存参数设置的也很大,内存没有爆掉,那么就来到了“返回响应给前端”这一步。

网络开销过大

我们都知道,网络传输的数据量越多,时延也会越高。

百万条记录在网络中的传输,究竟要多久呢?

以虚拟机为64位的机器为例,假设单个商品信息对象有8个字段且都是基本数据类型,对象头占用的内存是8(运行时数据)+4(类型指针)=12Byte,实例数据是(8个字段)8 * 4 = 32Byte,由于HotSpot VM的自动内存管理系统要求对象起始地址必须是8字节的整数倍,换句话说就是对象的大小必须是8字节的整数倍,所以按照一个商品信息对象 48Byte的大小进行计算,百万条数据的大小为:

48 * 100W = 45.77MB

传输时间过长

接下来,计算45.77MB在网络速度为100Mb/S的情况下的传输时长为:

45.77 MB* 8bit / 100Mb = 3.66s

实际消耗肯定比3.66s更长,因为有损耗

注意,这仅仅是数据在网络中传输需要的时长,在此之前,客户端和服务端还需要经历TCP三次握手建立连接等等,在前端接收到数据之后,还要对数据进行解析、格式处理等等…

即使假设系统优化做的非常好,客户端的网络、硬件配置也特别高,整个流程仅花费了网络传输的时长3.66s,也无法接受。

用户体验太差

根据1/3/5秒原则,在1s以内得到响应,用户会觉得系统响应很快,体验非常好;1-3秒得到响应,用户可以接收,体验还不错;3-5秒才响应,用户就感觉慢了,体验有点糟糕;一旦响应超过5秒,用户就会认为是个失败的体验,选择离开或重新发起请求。

所以,为什么要一次性返回百万级别的数据呢,如此庞大的网络开销,如此缓慢的响应时间,如此不可接受的用户体验…槽点太多以至于无法吐槽。

那么,我们必须思考一下,用户需要一次性查看这么多条数据吗?

显然不需要。用户想要查看所有的商品信息,但他不可能一目十行,而且手机显示屏也装不下百万条数据,所以他一定是分批次去查看数据。就好像我们看小说一样,一本书300W字,不可能在同一页面展示,所以我们一定是这页看完了再去看下一页。

二、有哪些分页方式?

分页的时间节点

对于用户来说,他并不关心分页怎么实现,但对程序员来说,分页的实现有多种选择。

以 “展示所有的商品信息” 为例,从请求发起到返回数据的整个过程如下图所示:

从上图观察得知,我们有三个节点可以进行分页处理,分别是:

  • 数据库分页
  • 后端逻辑分页
  • 前端逻辑分页

其中,数据库分页为物理分页,后端前端分页为逻辑分页

1.真分页(物理分页):

  • 实现原理:SELECT * FROM xxx [WHERE...] LIMIT #{param1}, #{param2}
    第一个参数是开始数据的索引位置
    第二个参数是要查询多少条数据
  • 优点: 不会造成内存溢出
  • 缺点: 翻页的速度比较慢

2.假分页(逻辑分页):

  • 实现原理: 一次性将所有的数据查询出来放在内存之中,每次需要查询的时候就直接从内存之中去取出相应索引区间的数据
  • 优点: 分页的速度比较快
  • 缺点: 可能造成内存溢出

分页时间节点的选择

选择的标准是速度,显而易见,数据库,服务器和客户端之间是网络,如果网络传递的数据量越少,则客户端获得响应的速度越快。而且一般来说,数据库和服务器的处理能力一般比客户端要强很多。从这两点来看,客户端分页的方案是最不可取的。

其次就剩下了在服务器端分页和在数据库端分页两种方式了,如果选择在服务器端分页的话,大部分的被过滤掉的数据还是被传输到了服务器端,与其这样还不如直接在数据库端进行分页。

前端要做的就是尽快接受数据并最快地展示给用户,对于数据不多的场景用前端实现也无妨,然而若考虑到以后会有成千上万条数据应用的场景,显然后端去处理分页更合适些。
每次点击下一页,前端只需发送分页数信息请求后端数据,假设一页显示十条数据,每次点击只需请求这十条数据的信息返回给前端来更快地进行交互。然而若是由前端来进行分页操作,那就得把成千上万条数据全部先拉下来,再进行操作,先不说操作这么多数据拉低的性能,光是先拉下来就得费很长时间了,所以对于数据量大的操作,一般都采用后端分页的操作更合适。
首先要了解为什么要分页。分页主要是为了避免一次性从数据库获取大量数据。其次才是为了展示效果。

因此:数据库端分页 > 后端分页 > 前端分页

 三、各个分页方式的实现

数据库分页(以MySQL为例)

1.LIMIT用法

LIMIT出现在查询语句的最后,可以使用一个参数或两个参数来限制取出的数据。其中第一个参数代表偏移量:offset(可选参数),第二个参数代表取出的数据条数:rows。

  • 单参数用法

当指定一个参数时,默认省略了偏移量,即偏移量为0,从第一行数据开始取,一共取rows条。

/* 查询前五条数据 */

SELECT * FROM Student LIMIT 5;

  • 双参数用法

当指定两个参数时,需要注意偏移量的取值是从0开始的,此时可以有两种写法:

/* 查询第1-10条数据 */

SELECT * FROM Student LIMIT 0,10;


/* 查询第11-20条数据 */

SELECT * FROM Student LIMIT 10 OFFSET 10;

先放链接,一位大佬行云流水的文章:

深入理解什么是端口(port) – 知乎 (zhihu.com)

 

前言

为了观察服务是否启动以及排查异常情况,我们常常需要查看服务器的日志信息,涉及到Linux一直是我的弱点,而本篇文章 《究竟什么是端口》则是源于服务发布时,通过tail -f info.log 观察实时日志,我无法确定服务是否正常启动,同事则给我推荐观察服务端口的方式来验证

通过端口观察服务是否正常启动,其操作十分简单:使用netstas -tunlp命令

本篇内容的重点并非Linux命令,因此其他说明略

起源

我第一次折腾阿里云服务器时,挂在云服务器上的网站打不开,当时的阿里技术人员让我看看是不是端口没打开;小时候玩电脑,有时网络连接不上,技术人员也会让我看看是不是网线端口没有插好。那时不懂端口分物理端口和逻辑端口,后来学习了TCP/IP协议,跑起来第一个Tomcat的8080应用,逐渐理解了端口类似于门户的概念,实际上也是一知半解,因此今天决意深入了解。

物理端口和逻辑端口

物理是用于连接物理设备之间的接口,逻辑是逻辑上用于区分服务的端口。

物理端口不作说明,下文详细介绍逻辑端口

常见端口

  • mysql 缺省用的 3306 端口,
  • redis 的 6379 端口,
  • tomcat 默认用的 8080 端口,
  • ssh 用的 22 端口,
  • 等等…

端口是必须的吗?

在本地 web 开发调试过程中, 我们可能都碰到过端口, 比如或许是/最著名的 8080 端口, 一般我们会这样去访问本地的 web 程序:

localhost:8080

但一旦 web 程序部署到了正式的网站中, 端口似乎就消失了, 正式的网址中就不需要端口了吗? 答案是否定的, 在这里起作用的是缺省值.

比如你访问我的网站: xiaogd.net, 这个 url 中似乎没有端口, 但其实是有的, 它有一个默认值 443, 所以完整的形式实际是这样的:

xiaogd.net:443.

你可以通过 Chrome 的开发人员调试工具看到这一点:

可以看到, ip 地址后面跟着一个 443

如果你输入一个错误的端口, 比如 80, 像这样: xiaogd.net:80, 结果就是无法访问.

但是如果你改成 xiaogd.net:80, 它又可以访问了.

注意, 因为我服务器后台配置了 http 自动跳转 https 的 301 重定向, 所以最终浏览器会再次跳转到 xiaogd.net:443.
注意勾选 ‘Preserve log’ 以保留日志, 可以看到第一个 80 端口的请求会被响应一个 301 跳转, 并指示跳转目标, 也即是 Location 字段中的 https 请求, 浏览器接收此跳转指示并重新发起 https 请求, 也即是图中第二个 xiaogd.net 的请求. 所以地址栏最终还是会变成 https 的, 特此说明.

此时如果你输入 xiaogd.net:443, 它又不能访问了…

那么原因是什么呢? 你找到规律了没有?

注意一个是 http, 一个是 https.

注:HTTP的端口缺省值是80,HTTPS的端口缺省值是443

  • https://www.baidu.com:443/
  • http://www.baidu.com:80/

这两种方式都能正常访问百度,平时我们是省略端口号,而访问时浏览器会根据所用的协议为我们加上缺省端口,因此,端口不仅是必须的,而且还是必须正确的。

那为什么我们平时在访问百度的时候,只需要输入”百度”两个字就可以了呢?

那是开发者为用户考虑。把用户当成傻瓜和懒汉就对了,如果每次访问百度都需要加上https,.com,443这些信息,无疑增加了用户负担。

为什么一定需要端口?

为了实现网络进程通信,利用三元组(ip地址,传输层协议,端口)可以标识网络的进程,网络中的进程通信就可以利用这个标志与其它进程进行交互。

进程间通讯(IPC)

你在浏览器地址栏输入某个网站的域名, 然后回车, 就生成了一次请求, 然后服务器响应你的请求, 浏览器再把结果渲染出来, 你就能最终看到到一个网页。

如果你曾经 ping 过一个域名, 比如你现在 ping 我的域名 xiaogd.net, 你就能得到一个 ip 地址, 118.89.55.54:

有了 ip, 浏览器自然就能找到我的主机, 但还是有个问题, 我的主机上运行着好多的进程, 好多的服务, 除了最常见的 web 服务, 我可能还有 ftp 服务, mysql 服务等等不一而足.

简单地讲, 如果一个请求只有 ip 地址这一信息, 操作系统将不知道把这个请求交给哪个进程去处理, 如果是你来设计整个系统, 你想象一下, 是不是这样?

如果你仅仅是输入域名, 经过 DNS 解析后, 只能得到一个 IP 地址.

所谓的一次请求, 从一个比较底层的角度去看, 就是一次进程间的通讯.

它可以是 navicat 客户端与 mysql 数据库服务的一次通讯, 也可以是 winScp 客户端与 vsftpd FTP 服务的一次通讯等等.

以上面的具体为例, 可以说就是 Chrome 浏览器这个本地操作系统上的进程与我的服务器上的一个叫做 Nginx 的进程间的一次通讯.

那么, 所谓的端口, 其实可以简单地视作为进程 ID.

当然, 它与进程 ID 还是有不同的, 下面再分析, 或者目前你可以认为端口就是进程 ID 的影子.

也即是说, 如果仅有域名(ip), 是无法定位到一个进程的, 通讯的发起方不但需要给出 ip, 还需要给出端口, 只有这样, 服务器才能知道由哪个进程去响应。

注:输入域名,通过DNS解析,换取对应IP,通过IP地址可以找到目标主机,但无法得到具体要通信的进程,因此必须借助端口进行判断

端口, 一个间接层

那么问题又来了, 为什么引入端口, 而不是直接使用进程 ID 呢? 这个原因想想也不难明白, 大概有这么几点原因:

  1. 作为客户端无法知道服务端对应进程的 ID
  2. 服务端对应进程重启后 ID 会改变
  3. 一个网站的 web 进程 ID 是这个, 另一个网站的可能又是另一个

自然, 原因是很多的, 我也是随便的列举了一些, 你或许还能想到更多. 而为了解决这些个问题, 就引入了端口这一间接层(indirection).

计算机世界里有一句名言: 任何计算机问题均可通过增加一个间接(indirection)层来解决.(Any problem in computer science can be solved with another layer of indirection. — David Wheeler)
这个名言其实还有后面一句: But what usually will create another problem.(但通常会带来另一个问题)
这里所谓另一个问题, 比如它会使得层次结构复杂化, 交互效率下降等等. 当然了, 这就是架构师们要去权衡的问题了, 很多时候, 架构就是关于平衡的艺术. 打死都不肯引入任何的间接层, 这是一个极端; 而一上来就引入好多个间接层, 这又是另一个极端.

如果没有这个间接层, 客户端要与服务端通讯, 就要知道服务端对应进程的 ID, 也就是客户端是依赖于服务端的:

显然, 这种模式对于 web 这种一个服务端对应大量客户端访问的情形是极不适应的, 你都不知道有谁可能会来访问你的网站! 你根本无法告诉它们.

而有了端口这一间接层, 对于 web 的情形, 这种依赖被倒置了, 客户端总是把请求发送到 80(或443) 端口, 这些成为标准的一部分, 并要求服务端反过来去适应, 服务端去监听端口的通讯并处理, 变成了一种反向依赖.

如果一个进程想要提供 web 服务, 它启动之后就要去绑定(binding) web 相关的端口,

如果端口已经被其它进程绑定了(即所谓占用了), 就会绑定失败; 又或者被自身前一个未完全退出的进程占据着, 也会绑定失败, 在开发过程中你可能会遇到类似的问题, 一个 web 进程没有关闭, 你又试图启动另一个, 而两者都用了相同的端口, 就会产生冲突.

并在其上持续的监听(listen), 同时在有请求到来的时候去响应(response). 这样一来, 进程 ID 的问题就消解了:

这类似于一个接口回调, 浏览器只需要面向接口索取服务, 而无需知道接口服务的具体提供者, 这些细节被端口层所封装并隐藏起来了.端口这一间接层的存在解耦(decouple)了客户端与服务端之间的强依赖, 整个体系变得很灵活.

可以把端口视作一般编程概念中的接口(interface), 而像 Nginx, apache, tomcat 等等可以认为是这个接口的不同实现(Implementation).

注:如果使用PID来标识进程会产生关键问题——通信前服务端必须要告知客户端它所提供的服务的PID。这种方案显然不可行,因为服务端无法预知哪些客户端将连接它。同时,每次通信前服务端都要向客户端发送PID(因为PID是动态变化的),开销其实是很大的因此,我们应当将提供的服务门户固定,而PID是操作系统层面的,想要固定十分麻烦,于是引入端口的概念,且服务端提供服务的端口固定化,HTTP的服务就绑定在80端口上,HTTPS的服务就绑定在443端口上,IMPL邮件服务就绑定在143端口上。原本是客户端依赖于服务端,现在是服务端依赖于端口,即 依赖倒置。依赖倒置原则在Spring的IOC中也有体现,可以戳这里查看详情(42条消息) spring IOC详解(依赖倒置原则)_wang_nian的博客-CSDN博客_spring依赖倒置

端口与现实世界的一个类比

为加深理解, 可以举一个现实世界中的例子. 相信大家都有过去市民中心办事的经历, 比如去办理居住证, 护照, 社保等等业务, 你通常会收到一个小纸条让你去某个窗口办理对应业务, 这个窗口其实就类似于端口了:

比如 80 窗口就对应港澳台通行证业务

那么你要办港澳台通行证, 你就奔向 80 号窗口就完了. 你不要去问门口保卫处的王大爷, 到底是哪位同志办理这个业务.

今天可能是小明在办理, 隔了几天, 小明可能受伤了, 流血了, 又轮到小红在那里办理, 又过段时间, 小红也出意外了, 流产了, 又轮到小张在办理, 又过段时间, 小张被发现在办理业务过程中徇私舞弊, 流放了…

等等, 如果此时你的同事问你怎么办港澳台通行证, 你需要知道这些个人事变动的细节吗? 根本不需要呀, 你只需告诉他去 80 号窗口办理就好了…

市民中心的整个体系, 会确保有个会办理这些业务的人员坐在那个窗口下面, 你唯一需要做的, 就是到那个窗口下请求服务即可.

端口与名称服务(naming service)

通过上面现实世界类比的例子, 对于端口的机制, 相信你已经理解得比较深入了. 广义上讲, 端口层也可以视作一个 naming service(名称服务), 这与比如 spring cloud 中的 eureka 里的机制本质上是一样的, 只是这个 name 就是一个抽象的数字, 比如 80. 80 就代表了一个 web 服务, Nginx 之类的 web server 绑定并监听就相当于把自身提供的 web 服务注册于其上.

DNS 域名系统其实也是 naming service, 你通过 xiaogd.net 这个名字(name), 就能获取到我所提供给你的网页服务.
类似的还有 java 里的 JNDI 等, 把一个名字与一个服务关联起来, 比如一个名字就代表一个数据源(数据库连接)之类的.

端口与 IoC(控制反转)

广义上, 端口的上述机制也是控制反转(Ioc: Inversion of Control)思想的一种体现, 如果客户端需要知道服务端的进程 ID, 实际上就被服务端控制了, 毕竟我服务端在哪个 ID 上提供服务, 你就得把你的请求发到相应的 ID 上来;

而有了端口这一中间层呢? 作为客户端, 总是把请求发到对应端口上, 并要求服务端绑定并监听那些端口以及作出响应, 你服务端是反过来被我客户端所控制, 我客户端发到哪个端口, 你服务端就要去相应端口上监听并响应.

大家可以体会一下这种转变. 这种设计或思想在编程领域其实是特别重要的, 在很多其它地方都有体现.

因为浏览器总是把 web 请求发到了 80 或 443 端口, 这就要求一个 web server 进程去监听这些端口. 比如在我的服务器上, web server 是 Nginx, 它启动之后就会去监听 80 和 443 端口, 任何想要访问我的主页的人, 并不需要知道我的 Nginx 进程 ID 是啥, 借助于端口这一间接层, 你就能够与我的 Nginx 进程通讯, 并获取你想要的东西.

事实上你可以这么认为, 浏览器实际上只是在与端口通讯, 端口层再把这些请求委托(delegate)或代理(proxy)给相应的 web server 去处理, 端口的角色就是一个中间人, 一个间接层。

再论缺省端口

现在, 我们应该明白了, 端口是必要的了, 当然, 对最终的用户来说, 则不需要知道这些实现的细节, 对于他们, 应该遵循最小知识原则, 知道得越少越好.

如果你一定要让用户在输入 url 的过程中输入端口, 又或者要输入个 www 等等, 用户就要给你扔过来”十万个为什么”了…

为什么要加个 443?
为什么不是 334, 443是啥意思?
为什么一会儿是 80, 一会儿又是 443?
为什么加个 www, 啥意思?
为什么末尾还加个斜杠, 不加会死吗?

惹不起, 惹不起…

还记得前面说的, 用户是笨蛋, 用户是懒汉吗?

这里又要引用一句计算机世界的名言了: 程序员和上帝打赌要开发出更大更好连傻瓜都会用的软件, 而上帝却总能创造出更大更傻的傻瓜。目前为止,上帝赢了。
Programmers are in a race with the Universe to create bigger and better idiot-proof programs. The Universe is trying to create bigger and better idiots. So far the Universe is winning.

说句心里话, 很多时候, 用户能记住你的域名就阿弥陀佛了, 你就该烧高香了, 你还想用户记住你的端口, 真的想多了…

另一方面, 说到这里我们应该也能明白了, 那就是理论上, web 服务实际上可以构建在任何端口之上. 比如在本地开发的时候, 用户只有你自己, 那当然你可以随便挑一个端口, 比如 8080, 只要自己知道就好了或顶多告诉另一个与你配合的前端同事.

同理, 其它非 web 的服务, 比如 ftp 服务, 也不一定说非得在 21 端口上等等; mysql 服务的端口同样可以调整为 3306 之外的端口.

又或者说, 你想提供一个服务, 但只想小范围内的人知道, 你可以挑一个很偏门的端口, 这样一般人只输一个域名就没法访问到你的服务了.

比如有人想偷偷提供一些服务, 放一些广大淫民群众喜闻乐见的小视频啥的…刑法警告, 后果自负!! 别说我没有提醒你.

端口与 TCP/UDP 协议

前面一直在说, 什么 3306 端口, 80 端口, 443 端口, 其实严格来说, 端口是分 TCP 端口和 UDP 端口的, 不过多数时候遇到的都是 TCP 端口, 但 TCP 80 端口和 UDP 80 端口是不同的端口.

UDP 的 80 端口, 包括 443 端口其实被保留了, 目前的 http 协议只构建在 TCP 协议之上.
当然, 理论上讲, 在 UDP 上构建 http 也不能说就完全不行, 毕竟, 无论 UDP 还是 TCP 都是构建在 IP 协议之上, 总之呢, 计算机的世界没什么是不可能的, 而且似乎真有人在做这些尝试, 不过这就属于两小母牛对屁股–比较牛逼的范畴了, 深水区了, 咱也不懂, 不多说了.

还有一点, 对于进程间的端口通讯, 实际上是对称的, 也即是说, 服务器的响应也是先回到一个客户端的端口上.

如果你用 Windows 10 系统, 可以在 任务管理器 > 性能 > 打开资源监视器 > 网络 > TCP 连接, 点击下远程端口可以按照从小到大排列, 通常就可以看到 443 的相关连接了, 可以看到左边有一栏本地端口, 一个 TCP 连接总是有一个远程端口, 一个本地端口:

当发起一个 TCP 连接时, 客户端首先自己先随机挑选一个没有被使用的端口作为服务器响应的接收端口, 比如 38672. 在一个 TCP 的包里, 无论是握手包还是后续的数据包, 包头部分最重要的两个字段, 一个就是源端口(source port), 比如 38672; 另一个就是目标端口(destination port), 比如 80, 或者 443.

可以这样看, 服务器的响应也是先回到源端口, 比如 38672 上, 源端口再转给最终的进程, 比如浏览器.

而对于一个 IP 包, 同样的, 包头部分最重要的两个字段, 一个就是源IP(source IP); 另一个就是目标 IP(destination IP).

而 TCP 包会作为 IP 包的数据包被打包到 IP 包里面, 也就是说一个 IP 包里其实包含了 IP + 端口.

IP 加端口再加上端口与进程间的关联, 分属两个不同主机间的进程就能通过 TCP(UDP)/IP 协议愉快地进行进程间的通讯(IPC)了.

当然了, 同一个主机间的进程也同样可以利用这套机制. 但同一个主机间还可以有其它选择, 这个具体看各个操作系统是否提供相关机制及支持. 而 TCP/IP 属于广泛应用的标准协议, 从而得到了广泛支持.

0 83

如果你问我,实习期间最紧张最害怕的时刻是什么?

那一定是上线发版的时候。

 

年少轻狂三连击,服务全挂我挨批

公司的服务采用集群部署,发版时需要部署多台机器。当我第一次发版前,导师曾再三叮嘱我,项目发慢点,一台一台的发。那时我年少轻狂,觉得呆呆等着很无聊,于是一个三连击,仅有的三台机器同时发布,结果服务调用异常的报错铺天盖地而来

服务器部署服务的流程

服务部署在机器上,那么该机器就成了服务器,所有的用户请求都会发送到服务器上被处理。而当前互联网环境下,服务基本都是分布式/集群部署,也就是有多台服务器。各互联网企业在分布式/集群部署的环境下发布版本时,一定不会像我一样三连击,因为服务器部署服务的流程如下:

旧服务正在运行—>停掉旧服务—>部署新服务—>新服务启动

试想,三台服务器A,B,C工作的好好的,我先点击发布A机器,此时A机器先向注册中心发送消息——”我先走了哈,别把请求路由到我这里了”,然后Dubbo会将A机器从负载均衡策略中排除,如果A机器还有未处理的请求,它会先进行处理,之后服务彻底停止。(详细可参考Dubbo-优雅停机)停止后,A机器会部署新服务,也就是你发布的新版本,但是服务不是瞬间启动的它需要启动服务提供者,服务提供者在启动时还要向注册中心注册自己提供的服务,同时订阅自己需要的服务,等完成这些步骤后,A机器上的新服务才算是正式启动,才可以接收用户的请求(详细可参考Dubbo的简要执行流程)。显而易见,【停掉旧服务—>新服务启动】之间是需要耗费不少时间的,大概几分钟。

三连击为什么会导致服务挂掉呢?

原因就是 【停掉旧服务—>新服务启动】的这几分钟。A机器的服务还在启动中,此时A服务器不可用,而我又点击发布B机器,B机器的服务也进入了【停掉旧服务—>新服务启动】这个阶段,B服务器不可用,C机器同理。三连击导致A,B,C三台机器全部不可用,用户请求一过来就会报服务调用异常的错误

正确的流程应该是发布一台,观察一台的日志。A机器发布后,观察A机器的日志,确保A机器上的服务已经启动且有流量进入,这样的话不仅可以保证A机器可用,如果你的新版本出现了问题还可以立刻发现,此时就算需要回滚也只会影响A机器上的请求,B,C机器的服务是不受影响的,当然,最好的情况是任意一台机器都没有事故。

之所以服务器要进行分布式/集群部署,正是为了避免服务不可用。试想,如果淘宝618时用户无法下单,短短的几分钟可能会造成数以亿计的损失。所以一定要牢记,服务不可用是非常严重的事故。因此,涉及到集群环境下的服务部署,我们必须要考虑可用性及可靠性。

一台一台的发,我有一千台机器岂不是要发好几天?

非也,此处的一台一台发,强调的是一个百分比。我们知道,客户端请求会通过Dubbo的负载均衡策略选择路由到哪一台具体的服务器,如果你有A,B,C三台机器提供服务,一台一台的发可以保证任意时刻有2/3的机器可用,只要2/3的机器可以顶住当前的客户端流量,那就没有问题。(当然,如果2/3的机器顶不住,那你必须要加机器,不然的话流量会击垮B,C两台机器,服务彻底不可用)如果你有一千台机器,你完全可以一批10台或者20台的去发,尽管有10台或20台的机器不可用,但仍剩有99%或98%的机器可用,只要剩下的99%的机器可以顶得住当前流量,那就一点问题都没有。

一句话,多少台机器一起发,取决于发布时剩下可用的机器能否顶得住当前客户端流量。顶得住,没有任何问题;大大顶得住,甚至可以加大同时发布的机器数量,比如1000台只需要500台就足够顶住流量的话,直接50台50台的发布;顶不住的话,对不起,你还是老老实实的减少同时发布的机器数量吧~。

Dubbo-优雅停机

背景

对于任何一个线上应用,如何在服务更新部署过程中保证客户端无感知是开发者必须要解决的问题,即从应用停止到重启恢复服务这个阶段不能影响正常的业务请求。理想条件下,在没有请求的时候再进行更新是最安全可靠的,然而互联网应用必须要保证可用性,因此在技术层面上优化应用更新流程来保证服务在更新时无损是必要的。

传统的解决方式是通过将应用更新流程划分为手工摘流量、停应用、更新重启三个步骤,由人工操作实现客户端无对更新感知。这种方式简单而有效,但是限制较多:不仅需要使用借助网关的支持来摘流量,还需要在停应用前人工判断来保证在途请求已经处理完毕。这种需要人工介入的方式运维复杂度较高,只能适用规模较小的应用,无法在大规模系统上使用。

因此,如果在容器/框架级别提供某种自动化机制,来自动进行摘流量并确保处理完以到达的请求,不仅能保证业务不受更新影响,还可以极大地提升更新应用时的运维效率。

这个机制也就是优雅停机,目前Tomcat/Undertow/Dubbo等容器/框架都有提供相关实现。下面给出正式一些的定义:优雅停机是指在停止应用时,执行的一系列保证应用正常关闭的操作。这些操作往往包括等待已有请求执行完成、关闭线程、关闭连接和释放资源等,优雅停机可以避免非正常关闭程序可能造成数据异常或丢失,应用异常等问题。优雅停机本质上是JVM即将关闭前执行的一些额外的处理代码。

适用场景

  • JVM主动关闭(System.exit(int)
  • JVM由于资源问题退出(OOM);
  • 应用程序接受到SIGTERMSIGINT信号。

配置方式

服务的优雅停机

在Dubbo中,优雅停机是默认开启的,停机等待时间为10000毫秒。可以通过配置dubbo.service.shutdown.wait来修改等待时间。

例如将等待时间设置为20秒可通过增加以下配置实现:

dubbo.service.shutdown.wait=20000

容器的优雅停机

当使用org.apache.dubbo.container.Main这种容器方式来使用 Dubbo 时,也可以通过配置dubbo.shutdown.hooktrue来开启优雅停机。

通过QOS优雅上下线

基于ShutdownHook方式的优雅停机无法确保所有关闭流程一定执行完,所以 Dubbo 推出了多段关闭的方式来保证服务完全无损。

多段关闭即将停止应用分为多个步骤,通过运维自动化脚本或手工操作的方式来保证脚本每一阶段都能执行完毕。

在关闭应用前,首先通过 QOS 的offline指令下线所有服务,然后等待一定时间确保已经到达请求全部处理完毕,由于服务已经在注册中心下线,当前应用不会有新的请求。这时再执行真正的关闭(SIGTERM 或 SIGINT)流程,就能保证服务无损。

QOS可通过 telnet 或 HTTP 方式使用,具体方式请见Dubbo-QOS命令使用说明

流程

Provider在接收到停机指令后

  • 从注册中心上注销所有服务;
  • 从配置中心取消监听动态配置;
  • 向所有连接的客户端发送只读事件,停止接收新请求;
  • 等待一段时间以处理已到达的请求,然后关闭请求处理线程池;
  • 断开所有客户端连接。

Consumer在接收到停机指令后

  • 拒绝新到请求,直接返回调用异常;
  • 等待当前已发送请求执行完毕,如果响应超时则强制关闭连接。

当使用容器方式运行 Dubbo 时,在容器准备退出前,可进行一系列的资源释放和清理工。

例如使用 SpringContainer时,Dubbo 的ShutdownHook线程会执行ApplicationContextstopclose方法,保证 Bean的生命周期完整。

实现原理

Dubbo 优雅停机 | Apache Dubbo

 

Dubbo的简要启动流程

1. 服务器启动,运行服务提供者。

2. 服务提供者在启动时,向注册中心(zookeeper)注册自己提供的服务。

3. 服务消费者在启动时,向注册中心订阅自己所需的服务。

4. 注册中心返回服务提供者地址列表给消费者,(若有变更,注册中心将基于长连接推送变更数据给消费者)

5. 服务的消费者,从地址列表中,基于负载均衡,选一台提供者的服务器进行调用,若是失败,在从 地址列表中,选择另一台调用.

6. 期间Dubbo的监控中心,会记录定时消费者和提供者,的调用次数和时间

Dubbo的简要执行流程 – 简书 (jianshu.com)

0 96
/**
 * 获取CVS文件
 * @return 待刷入的数据
 */
private List<DistrictInfo> readByCSV(){
    log.info("获取CVS文件内容");
    ArrayList<DistrictInfo> list = new ArrayList<>();
    try (Reader reader = Files.newBufferedReader(Paths.get(""), Charset.forName("GBK"))) {
        Iterable<CSVRecord> records = CSVFormat.DEFAULT.parse(reader);
        for (CSVRecord record : records) {
            DistrictInfo districtInfo = new DistrictInfo();
            districtInfo.setRegionId(Integer.valueOf(record.get(0)));
            districtInfo.setRegionName(record.get(1));
            districtInfo.setpRegionId(Integer.valueOf(record.get(2)));
            districtInfo.setClasses(Integer.valueOf(record.get(3)));
            districtInfo.setStatus(Integer.valueOf(record.get(4)));
            districtInfo.setOperator(record.get(5));
            districtInfo.setMemo(record.get(6));
            districtInfo.setVersion(Integer.valueOf(record.get(7)));
            list.add(districtInfo);
        }
    } catch (IOException ex) {
        ex.printStackTrace();
    }
    return list;
}

/** 获取CVS文件 
 *  
 */
public void writeToCSV(List <QueryBusinessFieldListResp> list){
    final String FILE_NAME = "businessIDPointFromZT.csv";
    final String[] FILE_HEADER = {"BusinessID"};
    // 这里显式地配置一下CSV文件的Header,然后设置跳过Header(要不然读的时候会把头也当成一条记录)
    CSVFormat format = CSVFormat.DEFAULT.withHeader(FILE_HEADER).withSkipHeaderRecord();
    // 这是写入CSV的代码
    try(Writer out = new FileWriter(FILE_NAME);
        CSVPrinter printer = new CSVPrinter(out, format)) {
        for (QueryBusinessFieldListResp resp : list) {
            if (resp != null) {
                for (QueryBusinessFieldResp queryBusinessFieldResp : resp.getBusinessFieldRespList()) {
                    List<String> records = new ArrayList<>();
                    records.add(queryBusinessFieldResp.getBusinessId());
                    records.add(queryBusinessFieldResp.getValue().toString());
                    printer.printRecord(records);
                }
            }
        }
        } catch (IOException ioException) {
        ioException.printStackTrace();
    }
}

原来 8 张图,就可以搞懂「零拷贝」了 – 知乎 (zhihu.com)

深入剖析Linux IO原理和几种零拷贝机制的实现 – 知乎 (zhihu.com)

前情提要

在学习Netty时,了解到其底层模型是NIO,同时还用到了零拷贝技术,所以特此学习零拷贝。本文复制链接如上,注为我所做


一、DMA技术

磁盘可以说是计算机系统最慢的硬件之一,读写速度相差内存 10 倍以上,所以针对优化磁盘的技术非常的多,比如零拷贝、直接 I/O、异步 I/O 等等,这些优化的目的就是为了提高系统的吞吐量,另外操作系统内核中的磁盘高速缓存区,可以有效的减少磁盘的访问次数。

注:磁盘读写非常慢,因此出现了零拷贝、直接I/O、异步I/O、DMA、缓存等优化技术

这次,我们就以「文件传输」作为切入点,来分析 I/O 工作方式,以及如何优化传输文件的性能。

为什么要有 DMA 技术?

在没有 DMA 技术前,I/O 的过程是这样的:

  • CPU 发出对应的指令给磁盘控制器,然后返回;
  • 磁盘控制器收到指令后,于是就开始准备数据,会把数据放入到磁盘控制器的内部缓冲区中,然后产生一个中断
  • CPU 收到中断信号后,停下手头的工作,接着把磁盘控制器的缓冲区的数据一次一个字节地读进自己的寄存器,然后再把寄存器里的数据写入到内存,而在数据传输的期间 CPU 是无法执行其他任务的。

为了方便你理解,我画了一副图:

可以看到,整个数据的传输过程,都要需要 CPU 亲自参与搬运数据的过程,而且这个过程,CPU 是不能做其他事情的。

简单的搬运几个字符数据那没问题,但是如果我们用千兆网卡或者硬盘传输大量数据的时候,都用 CPU 来搬运的话,肯定忙不过来。

注:没有DMA技术时,CPU需要进行发送指令、响应IO中断、从磁盘控制器的缓冲区读取数据到CPU寄存器,再将数据写到用户缓冲区

计算机科学家们发现了事情的严重性后,于是就发明了 DMA 技术,也就是直接内存访问(Direct Memory Access 技术。

什么是 DMA 技术?简单理解就是,在进行 I/O 设备和内存的数据传输的时候,数据搬运的工作全部交给 DMA 控制器,而 CPU 不再参与任何与数据搬运相关的事情,这样 CPU 就可以去处理别的事务

那使用 DMA 控制器进行数据传输的过程究竟是什么样的呢?下面我们来具体看看。

具体过程:

  • 用户进程调用 read 方法,向操作系统发出 I/O 请求,请求读取数据到自己的内存缓冲区中,进程进入阻塞状态;
  • 操作系统收到请求后,进一步将 I/O 请求发送 DMA,然后让 CPU 执行其他任务;
  • DMA 进一步将 I/O 请求发送给磁盘;
  • 磁盘收到 DMA 的 I/O 请求,把数据从磁盘读取到磁盘控制器的缓冲区中,当磁盘控制器的缓冲区被读满后,向 DMA 发起中断信号,告知自己缓冲区已满;
  • DMA 收到磁盘的信号,将磁盘控制器缓冲区中的数据拷贝到内核缓冲区中,此时不占用 CPU,CPU 可以执行其他任务
  • 当 DMA 读取了足够多的数据,就会发送中断信号给 CPU;
  • CPU 收到 DMA 的信号,知道数据已经准备好,于是将数据从内核拷贝到用户空间,系统调用返回;

可以看到, 整个数据传输的过程,CPU 不再参与数据搬运的工作,而是全程由 DMA 完成,但是 CPU 在这个过程中也是必不可少的,因为传输什么数据,从哪里传输到哪里,都需要 CPU 来告诉 DMA 控制器。

早期 DMA 只存在在主板上,如今由于 I/O 设备越来越多,数据传输的需求也不尽相同,所以每个 I/O 设备里面都有自己的 DMA 控制器。

注:用户向CPU发起IO请求—>CPU向DMA发送IO请求—>DMA从磁盘中读取数据到磁盘控制器缓冲区,再写入到内核缓存区—>DMA发送中断信号给CPU—>CPU将数据从内核拷贝到用户空间,系统调用返回。
注:有了DMA,CPU只用发送IO请求,响应中断,把内核缓冲区的数据写入到用户缓冲区


二、传统的文件传输

如果服务端要提供文件传输的功能,我们能想到的最简单的方式是:将磁盘上的文件读取出来,然后通过网络协议发送给客户端。

传统 I/O 的工作方式是,数据读取和写入是从用户空间到内核空间来回复制,而内核空间的数据是通过操作系统层面的 I/O 接口从磁盘读取或写入。

代码通常如下,一般会需要两个系统调用:

read(file, tmp_buf, len);
write(socket, tmp_buf, len);

代码很简单,虽然就两行代码,但是这里面发生了不少的事情。

首先,期间共发生了 4 次用户态与内核态的上下文切换,因为发生了两次系统调用,一次是 read() ,一次是 write(),每次系统调用都得先从用户态切换到内核态,等内核完成任务后,再从内核态切换回用户态。

上下文切换到成本并不小,一次切换需要耗时几十纳秒到几微秒,虽然时间看上去很短,但是在高并发的场景下,这类时间容易被累积和放大,从而影响系统的性能。

其次,还发生了 4 次数据拷贝,其中两次是 DMA 的拷贝,另外两次则是通过 CPU 拷贝的,下面说一下这个过程:

  • 第一次拷贝,把磁盘上的数据拷贝到操作系统内核的缓冲区里,这个拷贝的过程是通过 DMA 搬运的。
  • 第二次拷贝,把内核缓冲区的数据拷贝到用户的缓冲区里,于是我们应用程序就可以使用这部分数据了,这个拷贝到过程是由 CPU 完成的。
  • 第三次拷贝,把刚才拷贝到用户的缓冲区里的数据,再拷贝到内核的 socket 的缓冲区里,这个过程依然还是由 CPU 搬运的。
  • 第四次拷贝,把内核的 socket 缓冲区里的数据,拷贝到网卡的缓冲区里,这个过程又是由 DMA 搬运的。

我们回过头看这个文件传输的过程,我们只是搬运一份数据,结果却搬运了 4 次,过多的数据拷贝无疑会消耗 CPU 资源,大大降低了系统性能。

这种简单又传统的文件传输方式,存在冗余的上文切换和数据拷贝,在高并发系统里是非常糟糕的,多了很多不必要的开销,会严重影响系统性能。

所以,要想提高文件传输的性能,就需要减少「用户态与内核态的上下文切换」和「内存拷贝」的次数

注:传统的文件传输发送了4次用户态和内核态的上下文切换(两次系统调用,一次read,一次write),还发生了4次数据拷贝(两次DMA拷贝,两次CPU拷贝)


三、如何优化文件传输的性能?

先来看看,如何减少「用户态与内核态的上下文切换」的次数呢?

读取磁盘数据的时候,之所以要发生上下文切换,这是因为用户空间没有权限操作磁盘或网卡,内核的权限最高,这些操作设备的过程都需要交由操作系统内核来完成,所以一般要通过内核去完成某些任务的时候,就需要使用操作系统提供的系统调用函数。

而一次系统调用必然会发生 2 次上下文切换:首先从用户态切换到内核态,当内核执行完任务后,再切换回用户态交由进程代码执行。

所以,要想减少上下文切换到次数,就要减少系统调用的次数

再来看看,如何减少「数据拷贝」的次数?

在前面我们知道了,传统的文件传输方式会历经 4 次数据拷贝,而且这里面,「从内核的读缓冲区拷贝到用户的缓冲区里,再从用户的缓冲区里拷贝到 socket 的缓冲区里」,这个过程是没有必要的。

因为文件传输的应用场景中,在用户空间我们并不会对数据「再加工」,所以数据实际上可以不用搬运到用户空间,因此用户的缓冲区是没有必要存在的

注:减少数据拷贝的次数。可以直接将数据从内核态拷贝到网卡缓冲区。

 


 

四、零拷贝(减少上下文切换和数据拷贝)

零拷贝技术实现的方式通常有 2 种:

  • mmap + write
  • sendfile

注:实际上可以有很多种方式,只要能减少上下文切换和数据拷贝的次数即可

下面就谈一谈,它们是如何减少「上下文切换」和「数据拷贝」的次数。

mmap + write

在前面我们知道,read() 系统调用的过程中会把内核缓冲区的数据拷贝到用户的缓冲区里,于是为了减少这一步开销,我们可以用 mmap() 替换 read() 系统调用函数。

buf = mmap(file, len);
write(sockfd, buf, len);

mmap() 系统调用函数会直接把内核缓冲区里的数据「映射」到用户空间,这样,操作系统内核与用户空间就不需要再进行任何的数据拷贝操作。

注:直接内存映射。Linux提供的mmap系统调用, 它可以将一段用户空间内存映射到内核空间, 当映射成功后, 用户对这段内存区域的修改可以直接反映到内核空间;同样地, 内核空间对这段区域的修改也直接反映用户空间。正因为有这样的映射关系, 就不需要在用户态(User-space)与内核态(Kernel-space) 之间拷贝数据, 提高了数据传输的效率,这就是内存直接映射技术

具体过程如下:

  • 应用进程调用了 mmap() 后,DMA 会把磁盘的数据拷贝到内核的缓冲区里。接着,应用进程跟操作系统内核「共享」这个缓冲区;
  • 应用进程再调用 write(),操作系统直接将内核缓冲区的数据拷贝到 socket 缓冲区中,这一切都发生在内核态,由 CPU 来搬运数据;
  • 最后,把内核的 socket 缓冲区里的数据,拷贝到网卡的缓冲区里,这个过程是由 DMA 搬运的。

我们可以得知,通过使用 mmap() 来代替 read(), 可以减少一次数据拷贝的过程。

但这还不是最理想的零拷贝,因为仍然需要通过 CPU 把内核缓冲区的数据拷贝到 socket 缓冲区里,而且仍然需要 4 次上下文切换,因为系统调用还是 2 次。

注:直接内存映射mmap()减少一次CPU拷贝

sendfile

在 Linux 内核版本 2.1 中,提供了一个专门发送文件的系统调用函数 sendfile(),函数形式如下:

#include <sys/socket.h>
ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);

它的前两个参数分别是目的端和源端的文件描述符,后面两个参数是源端的偏移量和复制数据的长度,返回值是实际复制数据的长度。

首先,它可以替代前面的 read()write() 这两个系统调用,这样就可以减少一次系统调用,也就减少了 2 次上下文切换的开销。

其次,该系统调用,可以直接把内核缓冲区里的数据拷贝到 socket 缓冲区里,不再拷贝到用户态,这样就只有 2 次上下文切换,和 3 次数据拷贝。如下图:

但是这还不是真正的零拷贝技术,如果网卡支持 SG-DMA(The Scatter-Gather Direct Memory Access)技术(和普通的 DMA 有所不同),我们可以进一步减少通过 CPU 把内核缓冲区里的数据拷贝到 socket 缓冲区的过程。

你可以在你的 Linux 系统通过下面这个命令,查看网卡是否支持 scatter-gather 特性:

$ ethtool -k eth0 | grep scatter-gather
scatter-gather: on

于是,从 Linux 内核 2.4 版本开始起,对于支持网卡支持 SG-DMA 技术的情况下, sendfile() 系统调用的过程发生了点变化,具体过程如下:

  • 第一步,通过 DMA 将磁盘上的数据拷贝到内核缓冲区里;
  • 第二步,缓冲区描述符和数据长度传到 socket 缓冲区,这样网卡的 SG-DMA 控制器就可以直接将内核缓存中的数据拷贝到网卡的缓冲区里,此过程不需要将数据从操作系统内核缓冲区拷贝到 socket 缓冲区中,这样就减少了一次数据拷贝;

所以,这个过程之中,只进行了 2 次数据拷贝,如下图:

这就是所谓的零拷贝(Zero-copy)技术,因为我们没有在内存层面去拷贝数据,也就是说全程没有通过 CPU 来搬运数据,所有的数据都是通过 DMA 来进行传输的。

零拷贝技术的文件传输方式相比传统文件传输的方式,减少了 2 次上下文切换和数据拷贝次数,只需要 2 次上下文切换和数据拷贝次数,就可以完成文件的传输,而且 2 次的数据拷贝过程,都不需要通过 CPU,2 次都是由 DMA 来搬运。

所以,总体来看,零拷贝技术可以把文件传输的性能提高至少一倍以上


使用零拷贝技术的项目

Kafka、Nginx等


PageCache 有什么作用?(磁盘高速缓存)

回顾前面说道文件传输过程,其中第一步都是先需要先把磁盘文件数据拷贝「内核缓冲区」里,这个「内核缓冲区」实际上是磁盘高速缓存(PageCache

由于零拷贝使用了 PageCache 技术,可以使得零拷贝进一步提升了性能,我们接下来看看 PageCache 是如何做到这一点的。

读写磁盘相比读写内存的速度慢太多了,所以我们应该想办法把「读写磁盘」替换成「读写内存」。于是,我们会通过 DMA 把磁盘里的数据搬运到内存里,这样就可以用读内存替换读磁盘。

但是,内存空间远比磁盘要小,内存注定只能拷贝磁盘里的一小部分数据。

那问题来了,选择哪些磁盘数据拷贝到内存呢?

我们都知道程序运行的时候,具有「局部性」,所以通常,刚被访问的数据在短时间内再次被访问的概率很高,于是我们可以用 PageCache 来缓存最近被访问的数据,当空间不足时淘汰最久未被访问的缓存。

所以,读磁盘数据的时候,优先在 PageCache 找,如果数据存在则可以直接返回;如果没有,则从磁盘中读取,然后缓存 PageCache 中。

还有一点,读取磁盘数据的时候,需要找到数据所在的位置,但是对于机械磁盘来说,就是通过磁头旋转到数据所在的扇区,再开始「顺序」读取数据,但是旋转磁头这个物理动作是非常耗时的,为了降低它的影响,PageCache 使用了「预读功能」

比如,假设 read 方法每次只会读 32 KB 的字节,虽然 read 刚开始只会读 0 ~ 32 KB 的字节,但内核会把其后面的 32~64 KB 也读取到 PageCache,这样后面读取 32~64 KB 的成本就很低,如果在 32~64 KB 淘汰出 PageCache 前,进程读取到它了,收益就非常大。

所以,PageCache 的优点主要是两个:

  • 缓存最近被访问的数据;
  • 预读功能;

这两个做法,将大大提高读写磁盘的性能。

但是,在传输大文件(GB 级别的文件)的时候,PageCache 会不起作用,那就白白浪费 DMA 多做的一次数据拷贝,造成性能的降低,即使使用了 PageCache 的零拷贝也会损失性能

这是因为如果你有很多 GB 级别文件需要传输,每当用户访问这些大文件的时候,内核就会把它们载入 PageCache 中,于是 PageCache 空间很快被这些大文件占满。

另外,由于文件太大,可能某些部分的文件数据被再次访问的概率比较低,这样就会带来 2 个问题:

  • PageCache 由于长时间被大文件占据,其他「热点」的小文件可能就无法充分使用到 PageCache,于是这样磁盘读写的性能就会下降了;
  • PageCache 中的大文件数据,由于没有享受到缓存带来的好处,但却耗费 DMA 多拷贝到 PageCache 一次;

所以,针对大文件的传输,不应该使用 PageCache,也就是说不应该使用零拷贝技术,因为可能由于 PageCache 被大文件占据,而导致「热点」小文件无法利用到 PageCache,这样在高并发的环境下,会带来严重的性能问题。


大文件传输用什么方式实现?

那针对大文件的传输,我们应该使用什么方式呢?

我们先来看看最初的例子,当调用 read 方法读取文件时,进程实际上会阻塞在 read 方法调用,因为要等待磁盘数据的返回,如下图:

具体过程:

  • 当调用 read 方法时,会阻塞着,此时内核会向磁盘发起 I/O 请求,磁盘收到请求后,便会寻址,当磁盘数据准备好后,就会向内核发起 I/O 中断,告知内核磁盘数据已经准备好;
  • 内核收到 I/O 中断后,就将数据从磁盘控制器缓冲区拷贝到 PageCache 里;
  • 最后,内核再把 PageCache 中的数据拷贝到用户缓冲区,于是 read 调用就正常返回了。

对于阻塞的问题,可以用异步 I/O 来解决,它工作方式如下图:

它把读操作分为两部分:

  • 前半部分,内核向磁盘发起读请求,但是可以不等待数据就位就可以返回,于是进程此时可以处理其他任务;
  • 后半部分,当内核将磁盘中的数据拷贝到进程缓冲区后,进程将接收到内核的通知,再去处理数据;

而且,我们可以发现,异步 I/O 并没有涉及到 PageCache,所以使用异步 I/O 就意味着要绕开 PageCache。

绕开 PageCache 的 I/O 叫直接 I/O,使用 PageCache 的 I/O 则叫缓存 I/O。通常,对于磁盘,异步 I/O 只支持直接 I/O。

前面也提到,大文件的传输不应该使用 PageCache,因为可能由于 PageCache 被大文件占据,而导致「热点」小文件无法利用到 PageCache。

于是,在高并发的场景下,针对大文件的传输的方式,应该使用「异步 I/O + 直接 I/O」来替代零拷贝技术

直接 I/O 应用场景常见的两种:

  • 应用程序已经实现了磁盘数据的缓存,那么可以不需要 PageCache 再次缓存,减少额外的性能损耗。在 MySQL 数据库中,可以通过参数设置开启直接 I/O,默认是不开启;
  • 传输大文件的时候,由于大文件难以命中 PageCache 缓存,而且会占满 PageCache 导致「热点」文件无法充分利用缓存,从而增大了性能开销,因此,这时应该使用直接 I/O。

另外,由于直接 I/O 绕过了 PageCache,就无法享受内核的这两点的优化:

  • 内核的 I/O 调度算法会缓存尽可能多的 I/O 请求在 PageCache 中,最后「合并」成一个更大的 I/O 请求再发给磁盘,这样做是为了减少磁盘的寻址操作;
  • 内核也会「预读」后续的 I/O 请求放在 PageCache 中,一样是为了减少对磁盘的操作;

于是,传输大文件的时候,使用「异步 I/O + 直接 I/O」了,就可以无阻塞地读取文件了。

所以,传输文件的时候,我们要根据文件的大小来使用不同的方式:

  • 传输大文件的时候,使用「异步 I/O + 直接 I/O」;
  • 传输小文件的时候,则使用「零拷贝技术」;

在 nginx 中,我们可以用如下配置,来根据文件的大小来使用不同的方式:

location /video/ { 
    sendfile on; 
    aio on; 
    directio 1024m; 
}

当文件大小大于 directio 值后,使用「异步 I/O + 直接 I/O」,否则使用「零拷贝技术」。


总结

早期 I/O 操作,内存与磁盘的数据传输的工作都是由 CPU 完成的,而此时 CPU 不能执行其他任务,会特别浪费 CPU 资源。

于是,为了解决这一问题,DMA 技术就出现了,每个 I/O 设备都有自己的 DMA 控制器,通过这个 DMA 控制器,CPU 只需要告诉 DMA 控制器,我们要传输什么数据,从哪里来,到哪里去,就可以放心离开了。后续的实际数据传输工作,都会由 DMA 控制器来完成,CPU 不需要参与数据传输的工作。

传统 IO 的工作方式,从硬盘读取数据,然后再通过网卡向外发送,我们需要进行 4 上下文切换,和 4 次数据拷贝,其中 2 次数据拷贝发生在内存里的缓冲区和对应的硬件设备之间,这个是由 DMA 完成,另外 2 次则发生在内核态和用户态之间,这个数据搬移工作是由 CPU 完成的。

为了提高文件传输的性能,于是就出现了零拷贝技术,它通过一次系统调用(sendfile 方法)合并了磁盘读取与网络发送两个操作,降低了上下文切换次数。另外,拷贝数据都是发生在内核中的,天然就降低了数据拷贝的次数。

Kafka 和 Nginx 都有实现零拷贝技术,这将大大提高文件传输的性能。

零拷贝技术是基于 PageCache 的,PageCache 会缓存最近访问的数据,提升了访问缓存数据的性能,同时,为了解决机械硬盘寻址慢的问题,它还协助 I/O 调度算法实现了 IO 合并与预读,这也是顺序读比随机读性能好的原因。这些优势,进一步提升了零拷贝的性能。

需要注意的是,零拷贝技术是不允许进程对文件内容作进一步的加工的,比如压缩数据再发送。

另外,当传输大文件时,不能使用零拷贝,因为可能由于 PageCache 被大文件占据,而导致「热点」小文件无法利用到 PageCache,并且大文件的缓存命中率不高,这时就需要使用「异步 IO + 直接 IO 」的方式。

在 Nginx 里,可以通过配置,设定一个文件大小阈值,针对大文件使用异步 IO 和直接 IO,而对小文件使用零拷贝。

注:零拷贝不会经过用户空间

零拷贝是从磁盘到内核缓冲区再到网卡
大文件传输室友异步IO+直接IO(未使用PageCache)

 

0 107

写代码之前,我想先整理一下思路。

假设你在开发一款《学生成绩管理系统》的web应用时,你对自己的开发内容是十分清楚的:

  1. web应用是B/S架构;B/S模式是指基于浏览器(Browser)服务器(Server)形式的应用
  2. 正因为web应用是B/S架构,所以你需要编写一些好看的网页,并使得这些网页具有功能入口,这部分称之为前端工作。
  3. 前端工程展示在浏览器上,那浏览器上的数据从何而来?往何处而去?答案就是服务器。因此,你需要将后端工程部署到服务器上使之运行,同时还需要在服务器上配置MySQL,Redis等。

不管先写前端还是先写后端,还是先去部署MySQL这样的服务到服务器上,至少你清楚你要做的事情。

可是面对RPC框架,我还不知道要做什么。我的解决办法是:从应用的核心功能出发去思考,即——远程方法调用。

根据RPC框架中角色的定义:

客户端(Client):服务调用方。最理想的情况是RPC Client在完全不知道有RPC框架存在的情况下发起对远程服务的调用。

服务端(Server):服务提供方。在RPC规范中,这个Server并不是提供RPC服务器IP、端口监听的模块。而是远程服务方法的具体实现(在JAVA中就是RPC服务接口的具体实现)。其中的代码是最普通的和业务相关的代码,甚至其接口实现类本身都不知道将被某一个RPC远程客户端调用。

至此,我们只需要牢记一个概念,RPC框架是支持客户端在不知道调用细节的情况下,调用存在于远程计算机上的某个资源(该资源由服务端实现),就像调用本地应用程序中的资源一样的框架。框架中的所有细节所有技术,都是为了实现这个概念。

为此,我们可以奠定我们开发的核心内容:

  1. 实现客户端
  2. 实现服务端
  3. 实现客户端对服务端提供的远程服务的调用

让我们从最简易的实现开始,一步步的去完善这个框架的细节,支持更多的功能~

0 134

准备秋招时看到Guide哥的RPC框架,感觉这个项目应该会很有竞争力,可惜还没来得及学习秋招就已经结束了,因此将其作为我的毕业设计项目。

 

RPC(Remote Procedure Call):远程过程调用,它是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的思想

通俗的描述是:客户端在不知道调用细节的情况下,调用存在于远程计算机上的某个资源,就像调用本地应用程序中的资源一样。

RPC框架屏蔽了实现细节,使得开发者的操作变得简单,不仅达到了远程过程调用的目的,还实现了服务发现、负载、容错、网络传输、序列化等功能。

RPC(Remote Procedure Call Protocol)远程过程调用协议。一个通俗的描述是:客户端在不知道调用细节的情况下,调用存在于远程计算机上的某个对象,就像调用本地应用程序中的对象一样。比较正式的描述是:一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议。那么我们至少从这样的描述中挖掘出几个要点:

  • RPC是协议:既然是协议就只是一套规范,那么就需要有人遵循这套规范来进行实现。目前典型的RPC实现包括:Dubbo、Thrift、GRPC、Hetty等。这里要说明一下,目前技术的发展趋势来看,实现了RPC协议的应用工具往往都会附加其他重要功能,例如Dubbo还包括了服务管理、访问权限管理等功能。
  • 网络协议和网络IO模型对其透明:既然RPC的客户端认为自己是在调用本地对象。那么传输层使用的是TCP/UDP还是HTTP协议,又或者是一些其他的网络协议它就不需要关心了。既然网络协议对其透明,那么调用过程中,使用的是哪一种网络IO模型调用者也不需要关心。
  • 信息格式对其透明:我们知道在本地应用程序中,对于某个对象的调用需要传递一些参数,并且会返回一个调用结果。至于被调用的对象内部是如何使用这些参数,并计算出处理结果的,调用方是不需要关心的。那么对于远程调用来说,这些参数会以某种信息格式传递给网络上的另外一台计算机,这个信息格式是怎样构成的,调用方是不需要关心的。
  • 应该有跨语言能力:为什么这样说呢?因为调用方实际上也不清楚远程服务器的应用程序是使用什么语言运行的。那么对于调用方来说,无论服务器方使用的是什么语言,本次调用都应该成功,并且返回值也应该按照调用方程序语言所能理解的形式进行描述。

那么上面的描述情况可以用下图表示:

2-2、RPC要素

当然,上图是作为RPC的调用者所观察到的现象(而实际情况是客户端或多或少的还是需要知道一些调用RPC的细节)。但是我们是要讲解RPC的基本概念,所以RPC协议内部是怎么回事就要说清楚:

  • Client:RPC协议的调用方。就像上文所描述的那样,最理想的情况是RPC Client在完全不知道有RPC框架存在的情况下发起对远程服务的调用。但实际情况来说Client或多或少的都需要指定RPC框架的一些细节。
  • Server:在RPC规范中,这个Server并不是提供RPC服务器IP、端口监听的模块。而是远程服务方法的具体实现(在JAVA中就是RPC服务接口的具体实现)。其中的代码是最普通的和业务相关的代码,甚至其接口实现类本身都不知道将被某一个RPC远程客户端调用。
  • Stub/Proxy:RPC代理存在于客户端,因为要实现客户端对RPC框架“透明”调用,那么客户端不可能自行去管理消息格式、不可能自己去管理网络传输协议,也不可能自己去判断调用过程是否有异常。这一切工作在客户端都是交给RPC框架中的“代理”层来处理的。
  • Message Protocol:在上文我们已经说到,一次完整的client-server的交互肯定是携带某种两端都能识别的,共同约定的消息格式。RPC的消息管理层专门对网络传输所承载的消息信息进行编号和解码操作。目前流行的技术趋势是不同的RPC实现,为了加强自身框架的效率都有一套(或者几套)私有的消息格式。例如前文所讲到的RMI框架使用的消息协议为JRMP;后文我们将详细讲解的RPC框架Thrift也有私有的消息协议,“- Transfer/Network Protocol”(当然它还支持一些通用的消息格式,如JSON)。
  • Transfer/Network Protocol:传输协议层负责管理RPC框架所使用的网络协议、网络IO模型。例如Hessian的传输协议基于HTTP(应用层协议);而Thrift的传输协议基于TCP(传输层协议)。传输层还需要统一RPC客户端和RPC服务端所使用的IO模型;常用的IO模型在之前已经详细讲解过了(可参见我之前的博文《架构设计:系统间通信(3)——IO通信模型和JAVA实践 上篇》)
  • Selector/Processor:存在于RPC服务端,由于服务器端某一个RPC接口的实现的特性(它并不知道自己是一个将要被RPC提供给第三方系统调用的服务)。所以在RPC框架中应该有一种“负责执行RPC接口实现”的角色。它负责了包括:管理RPC接口的注册、判断客户端的请求权限、控制接口实现类的执行在内的各种工作。
  • IDL:实际上IDL(接口定义语言)并不是RPC实现中所必须的。但是需要跨语言的RPC框架一定会有IDL部分的存在。这是因为要找到一个各种语言能够理解的消息结构、接口定义的描述形式。如果您的RPC实现没有考虑跨语言性,那么IDL部分就不需要包括,例如JAVA RMI因为就是为了在JAVA语言间进行使用,所以JAVA RMI就没有相应的IDL。
  • 一定要说明一点,不同的RPC框架实现都有一定设计差异。例如生成Stub的方式不一样,IDL描述语言不一样、服务注册的管理方式不一样、运行服务实现的方式不一样、采用的消息格式封装不一样、采用的网络协议不一样。但是基本的思路都是一样的,上图中的所列出的要素也都是具有的。

文章链接:架构设计:系统间通信(10)——RPC的基本概念 – 云+社区 – 腾讯云 (tencent.com)