笔记区域,未整理

只有通过这种协议,计算机才知道我们想让它做什么。

编译过程:
源文件 -> 词法分析 -> 语法分析 -> 语义分析 -> 代码生成 -> 目标文件

协议三要素:
语法,就是这一段内容要符合一定的规则和格式。
语义,就是这一段内容要代表某种意义。
顺序。

只有通过网络协议,才能使一大片机器互相协作、共同完成一件事。

除了DNS还有一个更加精准的HTTPDNS

DNS、HTTP、HTTPS所在的层我们称为应用层。

传输层有两种协议,一种是无连接的协议UDP,一种是面向连接的协议TCP。

传输层封装完毕后,浏览器会将包交给操作系统的网络层。网络层的协议是IP协议。在IP协议里面会有源IP地址,即浏览器所在机器的IP地址和目标IP地址。

而操作系统启动的时候,就会被DHCP协议配置IP地址,以及默认的网关的IP地址192.168.1.1。

MAC地址。

ARP协议。

路由表。    

路由协议,常用的有OSPF和BGP。

应用层:DHCP、HTTP、HTTPS、RTMP、P2P、DNS、GTP、RPC

传输层:UDP、TCP

网络层:ICMP、IP、OSPF、BGP、IPSec、GRE

链路层:ARP、VLAN、STP

物理层:网络跳线

为什么网络要分层呀?因为不同的层次之间有不同的沟通方式,这个叫作协议。

IP协议里面包含目标地址和源地址。

复杂的程序都要分层,这是程序设计的要求。

所有不能表示出层层封装含义的比喻,都是不恰当的。

只要是在网络上跑的包,都是完整的。可以有下层没上层,绝对不可能有上层没下层。

对TCP协议来说,三次握手也好,重试也好,只要想发出去包,就要有IP层和MAC层,不然是发不出去的。

什么叫二层设备呀,就是只把MAC头摘下来,看看到底是丢弃、转发,还是自己留着。那什么叫三层设备呢?就是把MAC头摘下来之后,再把IP头摘下来,看看到底是丢弃、转发,还是自己留着。

IP地址是一个网卡在网络世界的通讯地址,相当于我们现实世界的门牌号码。

在网络地址中,至少在当时设计的时候,对于A、B、 C类主要分两部分,前面一部分是网络号,后面一部分是主机号。

于是有了一个折中的方式叫作无类型域间选路,简称CIDR。

这种方式打破了原来设计的几类地址的做法,将32位的IP地址一分为二,前面是网络号,后面是主机号。

你如果注意观察的话可以看到,10.100.122.2/24,这个IP地址中有一个斜杠,斜杠后面有个数字24。这种地址表示形式,就是CIDR。后面24的意思是,32位中,前24位是网络号,后8位是主机号。

伴随着CIDR存在的,一个是广播地址,10.100.122.255。如果发送这个地址,所有10.100.122网络里面的机器都可以收到。另一个是子网掩码,255.255.255.0。

将子网掩码和IP地址进行AND计算。前面三个255,转成二进制都是1。1和任何数值取AND,都是原来数值,因而前三个数不变,为10.100.122。后面一个0,转换成二进制是0,0和任何数值取AND,都是0,因而最后一个数变为0,合起来就是10.100.122.0。这就是网络号。将子网掩码和IP地址按位计算AND,就可得到网络号。

不需要将十进制转换为二进制32位,就能明显看出192.168.0是网络号,后面是主机号。而整个网络里面的第一个地址192.168.0.1,往往就是你这个私有网络的出口地址。例如,你家里的电脑连接Wi-Fi,Wi-Fi路由器的地址就是192.168.0.1,而192.168.0.255就是广播地址。一旦发送这个地址,整个192.168.0网络里面的所有机器都能收到。

我们来看16.158.165.91/22这个CIDR。求一下这个网络的第一个地址、子网掩码和广播地址。

你要是上来就写16.158.165.1,那就大错特错了。

/22不是8的整数倍,不好办,只能先变成二进制来看。16.158的部分不会动,它占了前16位。中间的165,变为二进制为10100101。除了前面的16位,还剩6位。所以,这8位中前6位是网络号,16.158.<101001>,而<01>.91是机器号。

第一个地址是16.158.<101001><00>.1,即16.158.164.1。子网掩码是255.255.<111111><00>.0,即255.255.252.0。广播地址为16.158.<101001><11>.255,即16.158.167.255。

D类是组播地址。使用这一类地址,属于某个组的机器都能收到。

在IP地址的后面有个scope,对于eth0这张网卡来讲,是global,说明这张网卡是可以对外的,可以接收来自各个地方的包。对于lo来讲,是host,说明这张网卡仅仅可以供本机相互通信。

lo全称是loopback,又称环回接口,往往会被分配到127.0.0.1这个地址。这个地址用于本机通信,经过内核处理后直接返回,不会在任何网络中出现。

在IP地址的上一行是link/ether fa:16:3e:c7:79:75 brd ff:ff:ff:ff:ff:ff,这个被称为MAC地址,是一个网卡的物理地址,用十六进制,6个byte表示。

 一个网络包要从一个地方传到另一个地方,除了要有确定的地址,还需要有定位功能。 而有门牌号码属性的IP地址,才是有远程定位功能的。

MAC地址更像是身份证,是一个唯一的标识。它的唯一性设计是为了组网的时候,不同的网卡放在一个网络里面的时候,可以不用担心冲突。从硬件角度,保证不同的网卡有不同的标识。

解析完了MAC地址,我们再来看 <BROADCAST,MULTICAST,UP,LOWER_UP>是干什么的?这个叫作net_device flags,网络设备的状态标识。

UP表示网卡处于启动的状态;BROADCAST表示这个网卡有广播地址,可以发送广播包;MULTICAST表示网卡可以发送多播包;LOWER_UP表示L1是启动的,也即网线插着呢。MTU1500是指什么意思呢?是哪一层的概念呢?最大传输单元MTU为1500,这是以太网的默认值。

MTU是二层MAC层的概念。MAC层有MAC的头,以太网规定连MAC头带正文合起来,不允许超过1500个字节。正文里面有IP的头、TCP的头、HTTP的头。如果放不下,就需要分片来传输。

qdisc pfifo_fast是什么意思呢?qdisc全称是queueing discipline,中文叫排队规则。内核如果需要通过某个网络接口发送数据包,它都需要按照为这个接口配置的qdisc(排队规则)把数据包加入队列。

最简单的qdisc是pfifo,它不对进入的数据包做任何的处理,数据包采用先入先出的方式通过队列。pfifo_fast稍微复杂一些,它的队列包括三个波段(band)。在每个波段里面,使用先进先出规则。

三个波段(band)的优先级也不相同。band 0的优先级最高,band 2的最低。如果band 0里面有数据包,系统就不会处理band 1里面的数据包,band 1和band 2之间也是一样。

数据包是按照服务类型(Type of Service,TOS)被分配到三个波段(band)里面的。TOS是IP头里面的一个字段,代表了当前的包是高优先级的,还是低优先级的。

Linux首先会判断,要去的这个地址和我是一个网段的吗,或者和我的一个网卡是同一网段的吗?只有是一个网段的,它才会发送ARP请求,获取MAC地址。

Linux默认的逻辑是,如果这是一个跨网段的调用,它便不会直接将包发送到网络上,而是企图将包发送到网关。

不同系统的配置文件格式不同,但是无非就是CIDR、子网掩码、广播地址和网关地址。

动态主机配置协议(Dynamic Host Configuration Protocol),简称DHCP。

先吼一句,我来啦,有人吗?这时候的沟通基本靠“吼”。这一步,我们称为DHCP Discover。

新来的机器使用IP地址0.0.0.0发送了一个广播包,目的IP地址为255.255.255.255。广播包封装了UDP,UDP封装了BOOTP。其实DHCP是BOOTP的增强版,但是如果你去抓包的话,很可能看到的名称还是BOOTP协议。

只有MAC唯一,IP管理员才能知道这是一个新人,需要租给它一个IP地址,这个过程我们称为DHCP Offer。同时,DHCP Server为此客户保留为它提供的IP地址,从而不会为其他DHCP客户分配此IP地址。

当DHCP Server接收到客户机的DHCP request之后,会广播返回给客户机一个DHCP ACK消息包,表明已经接受客户机的选择,并将这一IP地址的合法租用信息和其他的配置信息都放入该广播包,发给客户机,欢迎它加入网络大家庭。

客户机会在租期过去50%的时候,直接向为其提供IP地址的DHCP Server发送DHCP request消息包。客户机接收到该服务器回应的DHCP ACK消息包,会根据包中所提供的新的租期以及其他已经更新的TCP/IP参数,更新自己的配置。这样,IP租用更新就完成了。

预启动执行环境(Pre-boot Execution Environment),简称PXE。

首先,启动BIOS。这是一个特别小的小系统,只能干特别小的一件事情。其实就是读取硬盘的MBR启动扇区,将GRUB启动起来;然后将权力交给GRUB,GRUB加载内核、加载作为根文件系统的initramfs文件;然后将权力交给内核;最后内核启动,初始化整个操作系统。

IP层要封装了MAC层才能将包放入物理层。

两台电脑已经构成了一个最小的局域网,也即LAN。

有一个叫作Hub的东西,也就是集线器。

MAC的全称是Medium Access Control,即媒体访问控制。

多路访问。

分多个车道。每个车一个车道,你走你的,我走我的。这在计算机网络里叫作信道划分。

今天单号出行,明天双号出行,轮着来。这在计算机网络里叫作轮流协议。

不管三七二十一,有事儿先出门,发现特堵,就回去。错过高峰再出。我们叫作随机接入协议。著名的以太网,用的就是这个方式。

这里用到一个物理地址,叫作链路层地址。但是因为第二层主要解决媒体接入控制的问题,所以它常被称为MAC地址。

接下来是类型,大部分的类型是IP数据包,然后IP里面包含TCP、UDP,以及HTTP等,这都是里层封装的事情。

有了这个目标MAC地址,数据包在链路上广播,MAC的网卡才能发现,这个包是给它的。MAC的网卡把包收进来,然后打开IP包,发现IP地址也是自己的,再打开TCP包,发现端口是自己,也就是80,而nginx就是监听80。

对于以太网,第二层的最后面是CRC,也就是循环冗余检测。

通过XOR异或的算法,来计算整个包是否在发送的过程中出现了错误。

这就是ARP协议,也就是已知IP地址,求MAC地址的协议。

一台MAC1电脑将一个包发送给另一台MAC2电脑,当这个包到达交换机的时候,一开始交换机也不知道MAC2的电脑在哪个口,所以没办法,它只能将包转发给除了来的那个口之外的其他所有的口。但是,这个时候,交换机会干一件非常聪明的事情,就是交换机会记住,MAC1是来自一个明确的口。以后有包的目的地址是MAC1的,直接发送到这个口就可以了。

当然,每个机器的IP地址会变,所在的口也会变,因而交换机上的学习的结果,我们称为转发表,是有一个过期时间的。

MAC层是用来解决多路访问的堵车问题的。

ARP是通过吼的方式来寻找目标MAC地址的,吼完之后记住一段时间,这个叫作缓存。

交换机是有MAC地址学习能力的,学完了它就知道谁在哪儿了,不用广播了。

这个时候,一个交换机肯定不够用,需要多台交换机,交换机之间连接起来,就形成一个稍微复杂的拓扑结构。

环路问题。

在数据结构中,有一个方法叫作最小生成树。有环的我们常称为图。将图中的环破了,就生成了树。在计算机网络中,生成树的算法叫作STP,全称Spanning Tree Protocol。

STP协议里面有很多概念:

Root Bridge,也就是根交换机。

Designated Bridges,有的翻译为指定交换机。

Bridge Protocol Data Units (BPDU) ,网桥协议数据单元。

Priority Vector,优先级向量。
就是一组ID数目,[Root Bridge ID, Root Path Cost, Bridge ID, and Port ID]。

物理隔离。

虚拟隔离,就是用我们常说的VLAN,或者叫虚拟局域网。

我们只需要在原来的二层的头上加一个TAG,里面有一个VLAN ID,一共12位。

因为12位可以划分4096个VLAN。

而且对于交换机来讲,每个VLAN的口都是可以重新设置的。一个财务走了,把他所在的作为的口从VLAN 30移除掉,来了一个程序员,坐在财务的位置上,就把这个口设置为VLAN 10,十分灵活。

对于支持VLAN的交换机,有一种口叫作Trunk口。它可以转发属于任何VLAN的口。交换机之间可以通过这种口相互连接。

当交换机的数目越来越多的时候,会遭遇环路问题,让网络包迷路,这就需要使用STP协议,通过华山论剑比武的方式,将有环路的图变成没有环路的树,从而解决环路问题。

交换机数目多会面临隔离问题,可以通过VLAN形成虚拟局域网,从而解决广播问题和安全问题。

ping是基于ICMP协议工作的。ICMP全称Internet Control Message Protocol,就是互联网控制报文协议。

ICMP报文是封装在IP包里面的。因为传输指令的时候,肯定需要源地址和目标地址。

最常用的类型是主动请求为8,主动请求的应答为0。

这种是主帅发起的,主动查看敌情,对应ICMP的查询报文类型。例如,常用的ping就是查询报文,是一种主动请求,并且获得主动应答的ICMP协议。所以,ping发的包也是符合ICMP协议格式的,只不过它在后面增加了自己的格式。

对ping的主动请求,进行网络抓包,称为ICMP ECHO REQUEST。同理主动请求的回复,称为ICMP ECHO REPLY。比起原生的ICMP,这里面多了两个字段,一个是标识符。这个很好理解,你派出去两队侦查兵,一队是侦查战况的,一队是去查找水源的,要有个标识才能区分。另一个是序号,你派出去的侦查兵,都要编个号。如果派出去10个,回来10个,就说明前方战况不错;如果派出去10个,回来2个,说明情况可能不妙。

在选项数据中,ping还会存放发送请求的时间值,来计算往返时间,说明路程的长短。

差错报文。

终点不可达为3,源抑制为4,超时为11,重定向为5。

第一种是终点不可达。具体的原因在代码中表示就是,网络不可达代码为0,主机不可达代码为1,协议不可达代码为2,端口不可达代码为3,需要进行分片但设置了不分片位代码为4。

第二种是源站抑制,也就是让源站放慢发送速度。    

第三种是时间超时,也就是超过网络包的生存时间还是没到。

第四种是路由重定向,也就是让下次发给另一个路由器。

差错报文的结构相对复杂一些。除了前面还是IP,ICMP的前8字节不变,后面则跟上出错的那个IP包的IP头和IP正文的前8个字节。

ping命令执行的时候,源主机首先会构建一个ICMP请求数据包,ICMP数据包内包含多个字段。最重要的是两个,第一个是类型字段,对于请求数据包而言该字段为 8;另外一个是顺序号,主要用于区分连续ping的时候发出的多个数据包。每发出一个请求数据包,顺序号会自动加1。为了能够计算往返时间RTT,它会在报文的数据部分插入发送时间。

然后,由ICMP协议将这个数据包连同地址192.168.1.2一起交给IP层。IP层将以192.168.1.2作为目的地址,本机IP地址作为源地址,加上一些其他控制信息,构建一个IP数据包。

Traceroute的第一个作用就是故意设置特殊的TTL,来追踪去往目的地时沿途经过的路由器。Traceroute的参数指向某个目的IP地址,它会发送一个UDP的数据包。将TTL设置成1,也就是说一旦遇到一个路由器或者一个关卡,就表示它“牺牲”了。

Traceroute还有一个作用是故意设置不分片,从而确定路径的MTU。

Traceroute:差错报文类型的使用。

当你的宿舍长能够上网之后,接下来,就是其他人的电脑怎么上网的问题。这就需要配置你们的网卡。当然DHCP是可以默认配置的。在进行网卡配置的时候,除了IP地址,还需要配置一个Gateway的东西,这个就是网关。

在MAC头里面,先是目标MAC地址,然后是源MAC地址,然后有一个协议类型,用来说明里面是IP协议。IP头里面的版本号,目前主流的还是IPv4,服务类型TOS在第三节讲ip addr命令的时候讲过,TTL在第7节讲ICMP协议的时候讲过。另外,还有8位标识协议。这里到了下一层的协议,也就是,是TCP还是UDP。最重要的就是源IP和目标IP。先是源IP地址,然后是目标IP地址。

在任何一台机器上,当要访问另一个IP地址的时候,都会先判断,这个目标IP地址,和当前机器的IP地址,是否在同一个网段。怎么判断同一个网段呢?需要CIDR和子网掩码,这个在第三节的时候也讲过了。

网关往往是一个路由器,是一个三层转发的设备。啥叫三层设备?前面也说过了,就是把MAC头和IP头都取下来,然后根据里面的内容,看看接下来把包往哪里转发的设备。

路由器是一台设备,它有五个网口或者网卡,相当于有五只手,分别连着五个局域网。每只手的IP地址都和局域网的IP地址相同的网段,每只手都是它握住的那个局域网的网关。

静态路由,其实就是在路由器上,配置一条一条规则。

之前我说过,MAC地址是一个局域网内才有效的地址。因而,MAC地址只要过网关,就必定会改变,因为已经换了局域网。两者主要的区别在于IP地址是否改变。不改变IP地址的网关,我们称为转发网关;改变IP地址的网关,我们称为NAT网关。

从这个过程可以看出,IP地址也会变。这个过程用英文说就是Network Address Translation,简称NAT。

你可以通过 https://www.whatismyip.com/ 查看自己的出口IP地址。

如果离开本局域网,就需要经过网关,网关是路由器的一个网口;

路由器是一个三层设备,里面有如何寻找下一跳的规则;

当一个入口的网络包送到路由器时,它会根据一个本地的转发信息库,来决定如何正确地转发流量。这个转发信息库通常被称为路由表。

在大学里面学习计算机网络与数据结构的时候,知道求最短路径常用的有两种方法,一种是Bellman-Ford算法,一种是Dijkstra算法。

第一大类的算法称为距离矢量路由(distance vector routing)。它是基于Bellman-Ford算法的。

这种算法的第二个问题是,每次发送的时候,要发送整个全局路由表。

第二大类算法是链路状态路由(link state routing),基于Dijkstra算法。

OSPF(Open Shortest Path First,开放式最短路径优先)就是这样一个基于链路状态路由协议,广泛应用在数据中心中的协议。由于主要用在数据中心内部,用于路由决策,因而称为内部网关协议(Interior Gateway Protocol,简称IGP)。

内部网关协议的重点就是找到最短的路径。在一个组织内部,路径最短往往最优。当然有时候OSPF可以发现多个最短的路径,可以在这多个路径中进行负载均衡,这常常被称为等价路由。

基于距离矢量路由算法的BGP。

但是外网的路由协议,也即国家之间的,又有所不同。我们称为外网路由协议(Border Gateway Protocol,简称BGP)。

在网络世界,这一个个国家成为自治系统AS(Autonomous System)。自治系统分几种类型。

Stub AS:对外只有一个连接。这类AS不会传输其他AS的包。例如,个人或者小公司的网络。

Multihomed AS:可能有多个连接连到其他的AS,但是大多拒绝帮其他的AS传输包。例如一些大公司的网络。

Transit AS:有多个连接连到其他的AS,并且可以帮助其他的AS传输包。例如主干网。

BGP又分为两类,eBGP和iBGP。自治系统间,边界路由器之间使用eBGP广播路由。内部网络也需要访问其他的自治系统。

BGP协议使用的算法是路径矢量路由协议(path-vector protocol)。它是距离矢量路由协议的升级版。

路由分静态路由和动态路由,静态路由可以配置复杂的策略路由,控制转发策略;

动态路由主流算法有两种,距离矢量算法和链路状态算法。基于两种算法产生两种协议,BGP协议和OSPF协议。

传输层里比较重要的两个协议,一个是TCP,一个是UDP。

一般面试的时候我问这两个协议的区别,大部分人会回答,TCP是面向连接的,UDP是面向无连接的。

所谓的建立连接,是为了在客户端和服务端维护连接,而建立一定的数据结构来维护双方交互的状态,用这样的数据结构来保证所谓的面向连接的特性。

TCP提供可靠交付。通过TCP连接传输的数据,无差错、不丢失、不重复、并且按序到达。我们都知道IP包是没有任何可靠性保证的,一旦发出去,就像西天取经,走丢了、被妖怪吃了,都只能随它去。但是TCP号称能做到那个连接维护的程序做的事情,这个下两节我会详细描述。而UDP继承了IP包的特性,不保证不丢失,不保证按顺序到达。

TCP是面向字节流的。发送的时候发的是一个流,没头没尾。IP包可不是一个流,而是一个个的IP包。之所以变成了流,这也是TCP自己的状态维护做的事情。而UDP继承了IP的特性,基于数据报的,一个一个地发,一个一个地收。

还有TCP是可以有拥塞控制的。它意识到包丢弃了或者网络的环境不好了,就会根据情况调整自己的行为,看看是不是发快了,要不要发慢点。UDP就不会,应用让我发,我就发,管它洪水滔天。

因而TCP其实是一个有状态服务。

我们可以这样比喻,如果MAC层定义了本地局域网的传输行为,IP层定义了整个网络端到端的传输行为,这两层基本定义了这样的基因:网络传输是以包为单位的,二层叫帧,网络层叫包,传输层叫段。我们笼统地称为包。包单独传输,自行选路,在不同的设备封装解封装,不保证到达。基于这个基因,生下来的孩子UDP完全继承了这些特性,几乎没有自己的思想。

所以在IP头里面有个8位协议,这里会存放,数据里面到底是TCP还是UDP,当然这里是UDP。于是,如果我们知道UDP头的格式,就能从数据里面,将它解析出来。

当我们看到UDP包头的时候,发现的确有端口号,有源端口号和目标端口号。

UDP的三大特点:
第一,沟通简单。
第二,轻信他人。
第三,愣头青,做事不懂权变。

UDP的三大使用场景:
第一,需要资源少,在网络情况比较好的内网,或者对于丢包不敏感的应用。
第二,不需要一对一沟通,建立连接,而是可以广播的应用。
第三,需要处理速度快,时延低,可以容忍少数丢包,但是要求即便网络拥塞,也毫不退缩,一往无前的时候。

而QUIC(全称Quick UDP Internet Connections,快速UDP互联网连接)是Google提出的一种基于UDP改进的通信协议,其目的是降低网络通信的延迟,提供更好的用户互动体验。

Google旗下的Nest建立Thread Group,推出了物联网通信协议Thread,就是基于UDP协议的。

UDP虽然简单,但它有简单的用法。它可以用在环境简单、需要多播、应用层自己控制传输的地方。例如DHCP、VXLAN、QUIC等。

它天然认为网络环境是恶劣的,丢包、乱序、重传,拥塞都是常有的事情,一言不合就可能送达不了,因而要从算法层面来保证可靠性。

首先,源端口号和目标端口号是不可少的,这一点和UDP是一样的。如果没有这两个端口号。数据就不知道应该发给哪个应用。

还应该有的就是确认序号。发出去的包应该有确认,要不然我怎么知道对方有没有收到呢?如果没有收到就应该重新发送,直到送达。这个可以解决不丢包的问题。

接下来有一些状态位。例如SYN是发起一个连接,ACK是回复,RST是重新连接,FIN是结束连接等。TCP是面向连接的,因而双方要维护连接的状态,这些带状态位的包的发送,会引起双方的状态变更。

还有一个重要的就是窗口大小。TCP要做流量控制,通信双方各声明一个窗口,标识自己当前能够的处理能力。

除了做流量控制以外,TCP还会做拥塞控制,对于真正的通路堵车不堵车,它无能为力,唯一能做的就是控制自己,也即控制发送的速度。

通过对TCP头的解析,我们知道要掌握TCP协议,重点应该关注以下几个问题:
顺序问题 ,稳重不乱;
丢包问题,承诺靠谱;
连接维护,有始有终;
流量控制,把握分寸;
拥塞控制,知进知退。

请求->应答->应答之应答

TCP包的序号的问题。

等待的时间设为2MSL,MSL是Maximum Segment Lifetime,报文最大生存时间,它是任何报文在网络上存在的最长时间,超过这个时间报文将被丢弃。因为TCP报文基于是IP协议的,而IP头中有一个TTL域,是IP数据报可以经过的最大路由数,每经过一个处理他的路由器此值就减1,当此值为0则数据报将被丢弃,同时发送ICMP报文通知源主机。协议规定MSL为2分钟,实际应用中常用的是30秒,1分钟和2分钟等。

还有一个异常情况就是,B超过了2MSL的时间,依然没有收到它发的FIN的ACK,怎么办呢?按照TCP的原理,B当然还会重发FIN,这个时候A再收到这个包之后,A就表示,我已经在这里等了这么长时间了,已经仁至义尽了,之后的我就都不认了,于是就直接发送RST,B就知道A早就跑了。

TCP协议使用的也是同样的模式。为了保证顺序性,每一个包都有一个ID。在建立连接的时候,会商定起始的ID是什么,然后按照ID一个个发送。为了保证不丢包,对于发送的包都要进行应答,但是这个应答也不是一个一个来的,而是会应答某个之前的ID,表示都收到了,这种模式称为累计确认或者累计应答(cumulative acknowledgment)。

为了记录所有发送的包和接收的包,TCP也需要发送端和接收端分别都有缓存来保存这些记录。发送端的缓存里是按照包的ID一个个排列,根据处理的情况分成四个部分(发送了并且已经确认的、发送了并且尚未确认的、没有发送,但是已经等待发送的、没有发送,并且暂时还不会发送的)。

到底一个员工能够同时处理多少事情呢?在TCP里,接收端会给发送端报一个窗口的大小,叫Advertised window。这个窗口的大小应该等于上面的第二部分加上第三部分,就是已经交代了没做完的加上马上要交代的。超过这个窗口的,接收端做不过来,就不能发送了。

那发送方怎么判断网络是不是满呢?这其实是个挺难的事情,因为对于TCP协议来讲,他压根不知道整个网络路径都会经历什么,对他来讲就是一个黑盒。TCP发送包常被比喻为往一个水管里面灌水,而TCP的拥塞控制就是在不堵塞,不丢包的情况下,尽量发挥带宽。

于是TCP的拥塞控制主要来避免两种现象,包丢失和超时重传。

如果我们通过漏斗往瓶子里灌水,我们就知道,不能一桶水一下子倒进去,肯定会溅出来,要一开始慢慢的倒,然后发现总能够倒进去,就可以越倒越快。这叫作慢启动。

Socket编程进行的是端到端的通信,往往意识不到中间经过多少局域网,多少路由器,因而能够设置的参数,也只能是端到端协议之上网络层和传输层的。

在网络层,Socket函数需要指定到底是IPv4还是IPv6,分别对应设置为AF_INET和AF_INET6。另外,还要指定到底是TCP还是UDP。还记得咱们前面讲过的,TCP协议是基于数据流的,所以设置为SOCK_STREAM,而UDP是基于数据报的,因而设置为SOCK_DGRAM。

TCP的服务端要先监听一个端口,一般是先调用bind函数,给这个Socket赋予一个IP地址和端口。为什么需要端口呢?要知道,你写的是一个应用程序,当一个网络包来的时候,内核要通过TCP头里面的这个端口,来找到你这个应用程序,把包给你。为什么要IP地址呢?有时候,一台机器会有多个网卡,也就会有多个IP地址,你可以选择监听所有的网卡,也可以选择监听一个网卡,这样,只有发给这个网卡的包,才会给你。

当服务端有了IP和端口号,就可以调用listen函数进行监听。在TCP的状态图里面,有一个listen状态,当调用这个函数之后,服务端就进入了这个状态,这个时候客户端就可以发起连接了。

在内核中,为每个Socket维护两个队列。一个是已经建立了连接的队列,这时候连接三次握手已经完毕,处于established状态;一个是还没有完全建立连接的队列,这个时候三次握手还没完成,处于syn_rcvd的状态。

接下来,服务端调用accept函数,拿出一个已经完成的连接进行处理。如果还没有完成,就要等着。

在服务端等待的时候,客户端可以通过connect函数发起连接。先在参数中指明要连接的IP地址和端口号,然后开始发起三次握手。内核会给客户端分配一个临时的端口。一旦握手成功,服务端的accept就会返回另一个Socket。

这是一个经常考的知识点,就是监听的Socket和真正用来传数据的Socket是两个,一个叫作监听Socket,一个叫作已连接Socket。

连接建立成功之后,双方开始通过read和write函数来读写数据,就像往一个文件流里面写东西一样。

我们先来算一下理论值,也就是最大连接数,系统会用一个四元组来标识一个TCP连接。

{本机IP, 本机端口, 对端IP, 对端端口}
服务器通常固定在某个本地端口上监听,等待客户端的连接请求。因此,服务端端TCP连接四元组中只有对端IP, 也就是客户端的IP和对端的端口,也即客户端的端口是可变的,因此,最大TCP连接数=客户端IP数×客户端端口数。对IPv4,客户端的IP数最多为2的32次方,客户端的端口数最多为2的16次方,也就是服务端单机最大TCP连接数,约为2的48次方。

当然,服务端最大并发TCP连接数远不能达到理论上限。首先主要是文件描述符限制,按照上面的原理,Socket都是文件,所以首先要通过ulimit配置文件描述符的数目;另一个限制是内存,按上面的数据结构,每个TCP连接都要占用一定内存,操作系统是有限的。

http://www.163.com 是个URL,叫作统一资源定位符。

HTTP的报文大概分为三大部分。第一部分是请求行,第二部分是请求的首部,第三部分才是请求的正文实体。

HTTP协议虽然很常用,也很复杂,重点记住GET、POST、 PUT、DELETE这几个方法,以及重要的首部字段;

HTTP 2.0通过头压缩、分帧、二进制编码、多路复用等技术提升性能;

QUIC协议通过基于UDP自定义的类似TCP的连接、重试、多路复用、流量控制技术,进一步提升性能。

加密分对称加密和非对称加密。对称加密效率高,但是解决不了密钥传输问题;非对称加密可以解决这个问题,但是效率不高。

非对称加密需要通过证书和权威机构来验证公钥的合法性。

HTTPS是综合了对称加密和非对称加密算法的HTTP协议。既保证传输安全,也保证传输效率。

每一张图片,我们称为一帧。只要每秒钟帧的数据足够多,也即播放得足够快。比如每秒30帧,以人的眼睛的敏感程度,是看不出这是一张张独立的图片的,这就是我们常说的帧率(FPS)。

每一张图片,都是由像素组成的,假设为1024*768(这个像素数不算多)。每个像素由RGB组成,每个8位,共24位。

人们想到了编码,就是看如何用尽量少的Bit数保存视频,使播放的时候画面看起来仍然很精美。编码是一个压缩的过程。

之所以能够对视频流中的图片进行压缩,因为视频和图片有这样一些特点。

空间冗余:图像的相邻像素之间有较强的相关性,一张图片相邻像素往往是渐变的,不是突变的,没必要每个像素都完整地保存,可以隔几个保存一个,中间的用算法计算出来。

时间冗余:视频序列的相邻图像之间内容相似。一个视频中连续出现的图片也不是突变的,可以根据已有的图片进行预测和推断。

视觉冗余:人的视觉系统对某些细节不敏感,因此不会每一个细节都注意到,可以允许丢失一些数据。

编码冗余:不同像素值出现的概率不同,概率高的用的字节少,概率低的用的字节多,类似霍夫曼编码(Huffman Coding)的思路。

网络协议将编码好的视频流,从主播端推送到服务器,在服务器上有个运行了同样协议的服务端来接收这些网络包,从而得到里面的视频流,这个过程称为接流。

服务端接到视频流之后,可以对视频流进行一定的处理,例如转码,也即从一个编码格式,转成另一种格式。因为观众使用的客户端千差万别,要保证他们都能看到直播。

流处理完毕之后,就可以等待观众的客户端来请求这些视频流。观众的客户端请求的过程称为拉流。

如果有非常多的观众,同时看一个视频直播,那都从一个服务器上拉流,压力太大了,因而需要一个视频的分发网络,将视频预先加载到就近的边缘节点,这样大部分观众看的视频,是从边缘节点拉取的,就能降低服务器的压力。

当观众的客户端将视频流拉下来之后,就需要进行解码,也即通过上述过程的逆过程,将一串串看不懂的二进制,再转变成一帧帧生动的图片,在客户端播放出来,这样你就能看到美女帅哥啦。

编码:如何将丰富多彩的图片变成二进制流?
虽然我们说视频是一张张图片的序列,但是如果每张图片都完整,就太大了,因而会将视频序列分成三种帧。

I帧,也称关键帧。里面是完整的图片,只需要本帧数据,就可以完成解码。

P帧,前向预测编码帧。P帧表示的是这一帧跟之前的一个关键帧(或P帧)的差别,解码时需要用之前缓存的画面,叠加上和本帧定义的差别,生成最终画面。

B帧,双向预测内插编码帧。B帧记录的是本帧与前后帧的差别。要解码B帧,不仅要取得之前的缓存画面,还要解码之后的画面,通过前后画面的数据与本帧数据的叠加,取得最终的画面。

在一帧中,分成多个片,每个片中分成多个宏块,每个宏块分成多个子块,这样将一张大的图分解成一个个小块,可以方便进行空间上的编码。

尽管时空非常立体的组成了一个序列,但是总归还是要压缩成一个二进制流。这个流是有结构的,是一个个的网络提取层单元(NALU,Network Abstraction Layer Unit)。变成这种格式就是为了传输,因为网络上的传输,默认的是一个个的包,因而这里也就分成了一个个的单元。

在NALU头里面,主要的内容是类型NAL Type。

0x07表示SPS,是序列参数集, 包括一个图像序列的所有信息,如图像尺寸、视频格式等。

0x08表示PPS,是图像参数集,包括一个图像的所有分片的所有相关信息,包括图像类型、序列号等。

RTMP是基于TCP的,因而肯定需要双方建立一个TCP的连接。在有TCP的连接的基础上,还需要建立一个RTMP的连接,也即在程序里面,你需要调用RTMP类库的Connect函数,显示创建一个连接。

分发网络分为中心和边缘两层。边缘层服务器部署在全国各地及横跨各大运营商里,和用户距离很近。中心层是流媒体服务集群,负责内容的转发。智能负载均衡系统,根据用户的地理位置信息,就近选择边缘服务器,为用户提供推/拉流服务。中心层也负责转码服务,例如,把RTMP协议的码流转换为HLS码流。

视频名词比较多,编码两大流派达成了一致,都是通过时间、空间的各种算法来压缩数据;

压缩好的数据,为了传输组成一系列NALU,按照帧和片依次排列;

排列好的NALU,在网络传输的时候,要按照RTMP包的格式进行包装,RTMP的包会拆分成Chunk进行传输;

推送到流媒体集群的视频流经过转码和分发,可以被客户端通过RTMP协议拉取,然后组合为NALU,解码成视频格式进行播放。

当然,最简单的方式就是通过HTTP进行下载。但是相信你有过这样的体验,通过浏览器下载的时候,只要文件稍微大点,下载的速度就奇慢无比。

还有种下载文件的方式,就是通过FTP,也即文件传输协议。FTP采用两个TCP连接来传输一个文件。

控制连接:服务器以被动的方式,打开众所周知用于FTP的端口21,客户端则主动发起连接。该连接将命令从客户端传给服务器,并传回服务器的应答。常用的命令有:list——获取文件目录;reter——取一个文件;store——存一个文件。

数据连接:每当一个文件在客户端与服务器之间传输时,就创建一个数据连接。

每传输一个文件,都要建立一个全新的数据连接。FTP有两种工作模式,分别是主动模式(PORT)和被动模式(PASV),这些都是站在FTP服务器的角度来说的。

主动模式下,客户端随机打开一个大于1024的端口N,向服务器的命令端口21发起连接,同时开放N+1端口监听,并向服务器发出 “port N+1” 命令,由服务器从自己的数据端口20,主动连接到客户端指定的数据端口N+1。

被动模式下,当开启一个FTP连接时,客户端打开两个任意的本地端口N(大于1024)和N+1。第一个端口连接服务器的21端口,提交PASV命令。然后,服务器会开启一个任意的端口P(大于1024),返回“227 entering passive mode”消息,里面有FTP服务器开放的用来进行数据传输的端口。客户端收到消息取得端口号之后,会通过N+1号端口连接服务器的端口P,然后在两个端口之间进行数据传输。

后来,一种创新的、称为P2P的方式流行起来。P2P就是peer-to-peer。资源开始并不集中地存储在某些设备上,而是分散地存储在多台设备上。这些设备我们姑且称为peer。

想要下载一个文件的时候,你只要得到那些已经存在了文件的peer,并和这些peer之间,建立点对点的连接,而不需要到中心服务器上,就可以就近下载文件。一旦下载了文件,你也就成为peer中的一员,你旁边的那些机器,也可能会选择从你这里下载文件,所以当你使用P2P软件的时候,例如BitTorrent,往往能够看到,既有下载流量,也有上传的流量,也即你自己也加入了这个P2P的网络,自己从别人那里下载,同时也提供给其他人下载。可以想象,这种方式,参与的人越多,下载速度越快,一切完美。

但是有一个问题,当你想下载一个文件的时候,怎么知道哪些peer有这个文件呢?

这就用到种子啦,也即咱们比较熟悉的.torrent文件。.torrent文件由两部分组成,分别是:announce(tracker URL)和文件信息。

文件信息里面有这些内容。

info区:这里指定的是该种子有几个文件、文件有多长、目录结构,以及目录和文件的名字。

Name字段:指定顶层目录名字。

每个段的大小:BitTorrent(简称BT)协议把一个文件分成很多个小段,然后分段下载。

段哈希值:将整个种子中,每个段的SHA-1哈希值拼在一起。

下载时,BT客户端首先解析.torrent文件,得到tracker地址,然后连接tracker服务器。tracker服务器回应下载者的请求,将其他下载者(包括发布者)的IP提供给下载者。下载者再连接其他下载者,根据.torrent文件,两者分别对方告知自己已经有的块,然后交换对方没有的数据。此时不需要其他服务器参与,并分散了单个线路上的数据流量,因此减轻了服务器的负担。

下载者每得到一个块,需要算出下载块的Hash验证码,并与.torrent文件中的对比。如果一样,则说明块正确,不一样则需要重新下载这个块。这种规定是为了解决下载内容的准确性问题。

从这个过程也可以看出,这种方式特别依赖tracker。tracker需要收集下载者信息的服务器,并将此信息提供给其他下载者,使下载者们相互连接起来,传输数据。虽然下载的过程是非中心化的,但是加入这个P2P网络的时候,都需要借助tracker中心服务器,这个服务器是用来登记有哪些用户在请求哪些资源。

所以,这种工作方式有一个弊端,一旦tracker服务器出现故障或者线路遭到屏蔽,BT工具就无法正常工作了。

于是,后来就有了一种叫作DHT(Distributed Hash Table)的去中心化网络。每个加入这个DHT网络的人,都要负责存储这个网络里的资源信息和其他成员的联系信息,相当于所有人一起构成了一个庞大的分布式存储数据库。

有一种著名的DHT协议,叫Kademlia协议。这个和区块链的概念一样,很抽象,我来详细讲一下这个协议。

任何一个BitTorrent启动之后,它都有两个角色。一个是peer,监听一个TCP端口,用来上传和下载文件,这个角色表明,我这里有某个文件。另一个角色DHT node,监听一个UDP的端口,通过这个角色,这个节点加入了一个DHT的网络。

在DHT网络里面,每一个DHT node都有一个ID。这个ID是一个很长的串。每个DHT node都有责任掌握一些知识,也就是文件索引,也即它应该知道某些文件是保存在哪些节点上。它只需要有这些知识就可以了,而它自己本身不一定就是保存这个文件的节点。

下载一个文件可以使用HTTP或FTP,这两种都是集中下载的方式,而P2P则换了一种思路,采取非中心化下载的方式;

P2P也是有两种,一种是依赖于tracker的,也即元数据集中,文件数据分散;另一种是基于分布式的哈希算法,元数据和文件数据全部分散。

在网络世界,也是这样的。你肯定记得住网站的名称,但是很难记住网站的IP地址,因而也需要一个地址簿,就是DNS服务器。

由此可见,DNS在日常生活中多么重要。每个人上网,都需要访问它,但是同时,这对它来讲也是非常大的挑战。一旦它出了故障,整个互联网都将瘫痪。另外,上网的人分布在全世界各地,如果大家都去同一个地方访问某一台服务器,时延将会非常大。因而,DNS服务器,一定要设置成高可用、高并发和分布式的。

于是,就有了这样树状的层次结构。

根DNS服务器 :返回顶级域DNS服务器的IP地址

顶级域DNS服务器:返回权威DNS服务器的IP地址

权威DNS服务器 :返回相应主机的IP地址

为了提高DNS的解析性能,很多网络都会就近部署DNS缓存服务器。于是,就有了以下的DNS解析流程。

1.电脑客户端会发出一个DNS请求,问www.163.com的IP是啥啊,并发给本地域名服务器 (本地DNS)。那本地域名服务器 (本地DNS) 是什么呢?如果是通过DHCP配置,本地DNS由你的网络服务商(ISP),如电信、移动等自动分配,它通常就在你网络服务商的某个机房。

2.本地DNS收到来自客户端的请求。你可以想象这台服务器上缓存了一张域名与之对应IP地址的大表格。如果能找到 www.163.com,它直接就返回IP地址。如果没有,本地DNS会去问它的根域名服务器:“老大,能告诉我www.163.com的IP地址吗?”根域名服务器是最高层次的,全球共有13套。它不直接用于域名解析,但能指明一条道路。

3.根DNS收到来自本地DNS的请求,发现后缀是 .com,说:“哦,www.163.com啊,这个域名是由.com区域管理,我给你它的顶级域名服务器的地址,你去问问它吧。”

4.本地DNS转向问顶级域名服务器:“老二,你能告诉我www.163.com的IP地址吗?”顶级域名服务器就是大名鼎鼎的比如 .com、.net、 .org这些一级域名,它负责管理二级域名,比如 163.com,所以它能提供一条更清晰的方向。

5.顶级域名服务器说:“我给你负责 www.163.com 区域的权威DNS服务器的地址,你去问它应该能问到。”

6.本地DNS转向问权威DNS服务器:“您好,www.163.com 对应的IP是啥呀?”163.com的权威DNS服务器,它是域名解析结果的原出处。为啥叫权威呢?就是我的域名我做主。

7.权威DNS服务器查询后将对应的IP地址X.X.X.X告诉本地DNS。

8.本地DNS再将IP地址返回客户端,客户端和目标建立连接。

站在客户端角度,这是一次DNS递归查询过程。因为本地DNS全权为它效劳,它只要坐等结果即可。在这个过程中,DNS除了可以通过名称映射为IP地址,它还可以做另外一件事,就是负载均衡。

假设全国有多个数据中心,托管在多个运营商,每个数据中心三个可用区(Available Zone)。对象存储通过跨可用区部署,实现高可用性。在每个数据中心中,都至少部署两个内部负载均衡器,内部负载均衡器后面对接多个对象存储的前置服务器(Proxy-server)。

对于不需要做全局负载均衡的简单应用来讲,yourcompany.com的权威DNS服务器可以直接将 object.yourcompany.com这个域名解析为一个或者多个IP地址,然后客户端可以通过多个IP地址,进行简单的轮询,实现简单的负载均衡。

但是对于复杂的应用,尤其是跨地域跨运营商的大型应用,则需要更加复杂的全局负载均衡机制,因而需要专门的设备或者服务器来做这件事情,这就是全局负载均衡器(GSLB,Global Server Load Balance)。

DNS是网络世界的地址簿,可以通过域名查地址,因为域名服务器是按照树状结构组织的,因而域名查找是使用递归的方法,并通过缓存的方式增强性能;

上一节我们知道了DNS的两项功能,第一是根据名称查到具体的地址,另外一个是可以针对多个地址做负载均衡,而且可以在多个地址中选择一个距离你近的地方访问。

它可以在本地做一个缓存,也就是说,不是每一个请求,它都会去访问权威DNS服务器,而是访问过一次就把结果缓存到自己本地,当其他人来问的时候,直接就返回这个缓存数据。

另外,有的运营商会把一些静态页面,缓存到本运营商的服务器内,这样用户请求的时候,就不用跨运营商进行访问,这样既加快了速度,也减少了运营商之间流量计算的成本。在域名解析的时候,不会将用户导向真正的网站,而是指向这个缓存的服务器。

前面讲述网关的时候,我们知道,出口的时候,很多机房都会配置NAT,也即网络地址转换,使得从这个网关出去的包,都换成新的IP地址,当然请求返回的时候,在这个网关,再将IP地址转换回去,所以对于访问来说是没有任何问题。

本地DNS服务器是由不同地区、不同运营商独立部署的。对域名解析缓存的处理上,实现策略也有区别,有的会偷懒,忽略域名解析结果的TTL时间限制,在权威DNS服务器解析变更的时候,解析结果在全网生效的周期非常漫长。但是有的时候,在DNS的切换中,场景对生效时间要求比较高。

例如双机房部署的时候,跨机房的负载均衡和容灾多使用DNS来做。当一个机房出问题之后,需要修改权威DNS,将域名指向新的IP地址,但是如果更新太慢,那很多用户都会出现访问异常。

从上一节的DNS查询过程来看,DNS的查询过程需要递归遍历多个DNS服务器,才能获得最终的解析结果,这会带来一定的时延,甚至会解析超时。

HTTPNDS其实就是,不走传统的DNS解析,而是自己搭建基于HTTP协议的DNS服务器集群,分布在多个地点和多个运营商。当客户端需要DNS解析的时候,直接通过HTTP协议进行请求这个服务器集群,得到就近的地址。

这就相当于每家基于HTTP协议,自己实现自己的域名解析,自己做一个自己的地址簿,而不使用统一的地址簿。但是默认的域名解析都是走DNS的,因而使用HTTPDNS需要绕过默认的DNS路径,就不能使用默认的客户端。使用HTTPDNS的,往往是手机应用,需要在手机端嵌入支持HTTPDNS的客户端SDK。

通过自己的HTTPDNS服务器和自己的SDK,实现了从依赖本地导游,到自己上网查询做旅游攻略,进行自由行,爱怎么玩怎么玩。这样就能够避免依赖导游,而导游又不专业,你还不能把他怎么样的尴尬。

在客户端的SDK里动态请求服务端,获取HTTPDNS服务器的IP列表,缓存到本地。随着不断地解析域名,SDK也会在本地缓存DNS域名解析的结果。

当手机应用要访问一个地址的时候,首先看是否有本地的缓存,如果有就直接返回。这个缓存和本地DNS的缓存不一样的是,这个是手机应用自己做的,而非整个运营商统一做的。如何更新、何时更新,手机应用的客户端可以和服务器协调来做这件事情。

如果本地没有,就需要请求HTTPDNS的服务器,在本地HTTPDNS服务器的IP列表中,选择一个发出HTTP的请求,会返回一个要访问的网站的IP列表。

当然,当所有这些都不工作的时候,可以切换到传统的LocalDNS来解析,慢也比访问不到好。那HTTPDNS是如何解决上面的问题的呢?

其实归结起来就是两大问题。一是解析速度和更新速度的平衡问题,二是智能调度的问题,对应的解决方案是HTTPDNS的缓存设计和调度设计。

HTTPDNS的缓存设计
解析DNS过程复杂,通信次数多,对解析速度造成很大影响。为了加快解析,因而有了缓存,但是这又会产生缓存更新速度不及时的问题。最要命的是,这两个方面都掌握在别人手中,也即本地DNS服务器手中,它不会为你定制,你作为客户端干着急没办法。

而HTTPDNS就是将解析速度和更新速度全部掌控在自己手中。一方面,解析的过程,不需要本地DNS服务递归的调用一大圈,一个HTTP的请求直接搞定,要实时更新的时候,马上就能起作用;另一方面为了提高解析速度,本地也有缓存,缓存是在客户端SDK维护的,过期时间、更新时间,都可以自己控制。

HTTPDNS的缓存设计策略也是咱们做应用架构中常用的缓存设计模式,也即分为客户端、缓存、数据源三层。

对于应用架构来讲,就是应用、缓存、数据库。常见的是Tomcat、Redis、MySQL。

对于HTTPDNS来讲,就是手机客户端、DNS缓存、HTTPDNS服务器。

例如DNS缓存在内存中,也可以持久化到存储上,从而APP重启之后,能够尽快从存储中加载上次累积的经常访问的网站的解析结果,就不需要每次都全部解析一遍,再变成缓存。这有点像Redis是基于内存的缓存,但是同样提供持久化的能力,使得重启或者主备切换的时候,数据不会完全丢失。

SDK中的缓存会严格按照缓存过期时间,如果缓存没有命中,或者已经过期,而且客户端不允许使用过期的记录,则会发起一次解析,保障记录是更新的。

解析可以同步进行,也就是直接调用HTTPDNS的接口,返回最新的记录,更新缓存;也可以异步进行,添加一个解析任务到后台,由后台任务调用HTTPDNS的接口。

同步更新的优点是实时性好,缺点是如果有多个请求都发现过期的时候,同时会请求HTTPDNS多次,其实是一种浪费。

同步更新的方式对应到应用架构中缓存的Cache-Aside机制,也即先读缓存,不命中读数据库,同时将结果写入缓存。

异步更新的优点是,可以将多个请求都发现过期的情况,合并为一个对于HTTPDNS的请求任务,只执行一次,减少HTTPDNS的压力。同时可以在即将过期的时候,就创建一个任务进行预加载,防止过期之后再刷新,称为预加载。

它的缺点是当前请求拿到过期数据的时候,如果客户端允许使用过期数据,需要冒一次风险。如果过期的数据还能请求,就没问题;如果不能请求,则失败一次,等下次缓存更新后,再请求方能成功。

异步更新的机制对应到应用架构中缓存的Refresh-Ahead机制,即业务仅仅访问缓存,当过期的时候定期刷新。在著名的应用缓存Guava Cache中,有个RefreshAfterWrite机制,对于并发情况下,多个缓存访问不命中从而引发并发回源的情况,可以采取只有一个请求回源的模式。在应用架构的缓存中,也常常用数据预热或者预加载的机制。

HTTPDNS的调度设计
由于客户端嵌入了SDK,因而就不会因为本地DNS的各种缓存、转发、NAT,让权威DNS服务器误会客户端所在的位置和运营商,而可以拿到第一手资料。

在客户端,可以知道手机是哪个国家、哪个运营商、哪个省,甚至哪个市,HTTPDNS服务端可以根据这些信息,选择最佳的服务节点返回。

如果有多个节点,还会考虑错误率、请求时间、服务器压力、网络状况等,进行综合选择,而非仅仅考虑地理位置。当有一个节点宕机或者性能下降的时候,可以尽快进行切换。

要做到这一点,需要客户端使用HTTPDNS返回的IP访问业务应用。客户端的SDK会收集网络请求数据,如错误率、请求时间等网络请求质量数据,并发送到统计后台,进行分析、聚合,以此查看不同的IP的服务质量。

在服务端,应用可以通过调用HTTPDNS的管理接口,配置不同服务质量的优先级、权重。HTTPDNS会根据这些策略综合地理位置和线路状况算出一个排序,优先访问当前那些优质的、时延低的IP地址。

HTTPDNS通过智能调度之后返回的结果,也会缓存在客户端。为了不让缓存使得调度失真,客户端可以根据不同的移动网络运营商WIFI的SSID来分维度缓存。不同的运营商或者WIFI解析出来的结果会不同。

传统的DNS有很多问题,例如解析慢、更新不及时。因为缓存、转发、NAT问题导致客户端误会自己所在的位置和运营商,从而影响流量的调度。

HTTPDNS通过客户端SDK和服务端,通过HTTP直接调用解析DNS的方式,绕过了传统DNS的这些缺点,实现了智能的调度。

当一个用户想访问一个网站的时候,指定这个网站的域名,DNS就会将这个域名解析为地址,然后用户请求这个地址,返回一个网页。就像你要买个东西,首先要查找商店的位置,然后去商店里面找到自己想要的东西,最后拿着东西回家。

全球有这么多的数据中心,无论在哪里上网,临近不远的地方基本上都有数据中心。是不是可以在这些数据中心里部署几台机器,形成一个缓存的集群来缓存部分数据,那么用户访问数据的时候,就可以就近访问了呢?

当然是可以的。这些分布在各个地方的各个数据中心的节点,就称为边缘节点。

由于边缘节点数目比较多,但是每个集群规模比较小,不可能缓存下来所有东西,因而可能无法命中,这样就会在边缘节点之上。有区域节点,规模就要更大,缓存的数据会更多,命中的概率也就更大。在区域节点之上是中心节点,规模更大,缓存数据更多。如果还不命中,就只好回源网站访问了。

这就是CDN的分发系统的架构。CDN系统的缓存,也是一层一层的,能不访问后端真正的源,就不打扰它。

这个负载均衡主要用来选择一个就近的同样运营商的服务器进行访问。你会发现,CDN分发网络也是一个分布在多个区域、多个运营商的分布式系统,也可以用相同的思路选择最合适的边缘节点。

在没有CDN的情况下,用户向浏览器输入www.web.com这个域名,客户端访问本地DNS服务器的时候,如果本地DNS服务器有缓存,则返回网站的地址;如果没有,递归查询到网站的权威DNS服务器,这个权威DNS服务器是负责web.com的,它会返回网站的IP地址。本地DNS服务器缓存下IP地址,将IP地址返回,然后客户端直接访问这个IP地址,就访问到了这个网站。

然而有了CDN之后,情况发生了变化。在web.com这个权威DNS服务器上,会设置一个CNAME别名,指向另外一个域名 www.web.cdn.com,返回给本地DNS服务器。

当本地DNS服务器拿到这个新的域名时,需要继续解析这个新的域名。这个时候,再访问的就不是web.com的权威DNS服务器了,而是web.cdn.com的权威DNS服务器,这是CDN自己的权威DNS服务器。在这个服务器上,还是会设置一个CNAME,指向另外一个域名,也即CDN网络的全局负载均衡器。

DN支持流媒体协议,例如前面讲过的RTMP协议。在很多情况下,这相当于一个代理,从上一级缓存读取内容,转发给用户。由于流媒体往往是连续的,因而可以进行预先缓存的策略,也可以预先推送到用户的客户端。

对于静态页面来讲,内容的分发往往采取拉取的方式,也即当发现未命中的时候,再去上一级进行拉取。但是,流媒体数据量大,如果出现回源,压力会比较大,所以往往采取主动推送的模式,将热点数据主动推送到边缘节点。

对于流媒体来讲,很多CDN还提供预处理服务,也即文件在分发之前,经过一定的处理。例如将视频转换为不同的码流,以适应不同的网络带宽的用户需求;再如对视频进行分片,降低存储压力,也使得客户端可以选择使用不同的码率加载不同的分片。这就是我们常见的,“我要看超清、标清、流畅等”。

对于流媒体CDN来讲,有个关键的问题是防盗链问题。因为视频是要花大价钱买版权的,为了挣点钱,收点广告费,如果流媒体被其他的网站盗走,在人家的网站播放,那损失可就大了。

最常用也最简单的方法就是HTTP头的refer字段, 当浏览器发送请求的时候,一般会带上referer,告诉服务器是从哪个页面链接过来的,服务器基于此可以获得一些信息用于处理。如果refer信息不是来自本站,就阻止访问或者跳到其它链接。

refer的机制相对比较容易破解,所以还需要配合其他的机制。

一种常用的机制是时间戳防盗链。使用CDN的管理员可以在配置界面上,和CDN厂商约定一个加密字符串。

客户端取出当前的时间戳,要访问的资源及其路径,连同加密字符串进行签名算法得到一个字符串,然后生成一个下载链接,带上这个签名字符串和截止时间戳去访问CDN。

在CDN服务端,根据取出过期时间,和当前 CDN 节点时间进行比较,确认请求是否过期。然后CDN服务端有了资源及路径,时间戳,以及约定的加密字符串,根据相同的签名算法计算签名,如果匹配则一致,访问合法,才会将资源返回给客户。

然而比如在电商仓库中,我在前面提过,有关生鲜的缓存就是非常麻烦的事情,这对应着就是动态的数据,比较难以缓存。怎么办呢?现在也有动态CDN,主要有两种模式。

一种为生鲜超市模式,也即边缘计算的模式。既然数据是动态生成的,所以数据的逻辑计算和存储,也相应的放在边缘的节点。其中定时从源数据那里同步存储的数据,然后在边缘进行计算得到结果。就像对生鲜的烹饪是动态的,没办法事先做好缓存,因而将生鲜超市放在你家旁边,既能够送货上门,也能够现场烹饪,也是边缘计算的一种体现。

另一种是冷链运输模式,也即路径优化的模式。数据不是在边缘计算生成的,而是在源站生成的,但是数据的下发则可以通过CDN的网络,对路径进行优化。因为CDN节点较多,能够找到离源站很近的边缘节点,也能找到离用户很近的边缘节点。中间的链路完全由CDN来规划,选择一个更加可靠的路径,使用类似专线的方式进行访问。

对于常用的TCP连接,在公网上传输的时候经常会丢数据,导致TCP的窗口始终很小,发送速度上不去。根据前面的TCP流量控制和拥塞控制的原理,在CDN加速网络中可以调整TCP的参数,使得TCP可以更加激进地传输数据。

可以通过多个请求复用一个连接,保证每次动态请求到达时。连接都已经建立了,不必临时三次握手或者建立过多的连接,增加服务器的压力。另外,可以通过对传输数据进行压缩,增加传输效率。

所有这些手段就像冷链运输,整个物流优化了,全程冷冻高速运输。不管生鲜是从你旁边的超市送到你家的,还是从产地送的,保证到你家是新鲜的。

CDN和电商系统的分布式仓储系统一样,分为中心节点、区域节点、边缘节点,而数据缓存在离用户最近的位置。

CDN最擅长的是缓存静态数据,除此之外还可以缓存流媒体数据,这时候要注意使用防盗链。它也支持动态数据的缓存,一种是边缘计算的生鲜超市模式,另一种是链路优化的冷链运输模式。

如果要访问外网,需要经过一个叫网关的东西,而网关往往是一个路由器。

数据中心里面是服务器。服务器被放在一个个叫作机架(Rack)的架子上面。

数据中心的入口和出口也是路由器,由于在数据中心的边界,就像在一个国家的边境,称为边界路由器(Border Router)。为了高可用,边界路由器会有多个。

一般家里只会连接一个运营商的网络,而为了高可用, 为了当一个运营商出问题的时候,还可以通过另外一个运营商来提供服务,所以数据中心的边界路由器会连接多个运营商网络。

既然是路由器,就需要跑路由协议,数据中心往往就是路由协议中的自治区域(AS)。数据中心里面的机器要想访问外面的网站,数据中心里面也是有对外提供服务的机器,都可以通过BGP协议,获取内外互通的路由信息。这就是我们常听到的多线BGP的概念。

如果数据中心非常简单,没几台机器,那就像家里或者宿舍一样,所有的服务器都直接连到路由器上就可以了。但是数据中心里面往往有非常多的机器,当塞满一机架的时候,需要有交换机将这些服务器连接起来,可以互相通信。

这些交换机往往是放在机架顶端的,所以经常称为TOR(Top Of Rack)交换机。这一层的交换机常常称为接入层(Access Layer)。注意这个接入层和原来讲过的应用的接入层不是一个概念。

当一个机架放不下的时候,就需要多个机架,还需要有交换机将多个机架连接在一起。这些交换机对性能的要求更高,带宽也更大。这些交换机称为汇聚层交换机(Aggregation Layer)。

数据中心里面的每一个连接都是需要考虑高可用的。这里首先要考虑的是,如果一台机器只有一个网卡,上面连着一个网线,接入到TOR交换机上。如果网卡坏了,或者不小心网线掉了,机器就上不去了。所以,需要至少两个网卡、两个网线插到TOR交换机上,但是两个网卡要工作得像一张网卡一样,这就是常说的网卡绑定(bond)。

这就需要服务器和交换机都支持一种协议LACP(Link Aggregation Control Protocol)。它们互相通信,将多个网卡聚合称为一个网卡,多个网线聚合成一个网线,在网线之间可以进行负载均衡,也可以为了高可用作准备。

网卡有了高可用保证,但交换机还有问题。如果一个机架只有一个交换机,它挂了,那整个机架都不能上网了。因而TOR交换机也需要高可用,同理接入层和汇聚层的连接也需要高可用性,也不能单线连着。

最传统的方法是,部署两个接入交换机、两个汇聚交换机。服务器和两个接入交换机都连接,接入交换机和两个汇聚都连接,当然这样会形成环,所以需要启用STP协议,去除环,但是这样两个汇聚就只能一主一备了。STP协议里我们学过,只有一条路会起作用。

交换机有一种技术叫作堆叠,所以另一种方法是,将多个交换机形成一个逻辑的交换机,服务器通过多根线分配连到多个接入层交换机上,而接入层交换机多根线分别连接到多个交换机上,并且通过堆叠的私有协议,形成双活的连接方式。

由于对带宽要钱求更大,而且挂了影响也更大,所以两个堆叠可能就不够了,可以就会有更多的,比如四个堆叠为一个逻辑的交换机。

汇聚层将大量的计算节点相互连接在一起,形成一个集群。在这个集群里面,服务器之间通过二层互通,这个区域常称为一个POD(Point Of Delivery),有时候也称为一个可用区(Available Zone)。

当节点数目再多的时候,一个可用区放不下,需要将多个可用区连在一起,连接多个可用区的交换机称为核心交换机。

核心交换机吞吐量更大,高可用要求更高,肯定需要堆叠,但是往往仅仅堆叠,不足以满足吞吐量,因而还是需要部署多组核心交换机。核心和汇聚交换机之间为了高可用,也是全互连模式的。

这个时候还存在那个问题,出现环路怎么办?

一种方式是,不同的可用区在不同的二层网络,需要分配不同的网段。汇聚和核心之间通过三层网络互通的,二层都不在一个广播域里面,不会存在二层环路的问题。三层有环是没有问题的,只要通过路由协议选择最佳的路径就可以了。那为啥二层不能有环路,而三层可以呢?你可以回忆一下二层环路的情况。

如图,核心层和汇聚层之间通过内部的路由协议OSPF,找到最佳的路径进行访问,而且还可以通过ECMP等价路由,在多个路径之间进行负载均衡和高可用。

但是随着数据中心里面的机器越来越多,尤其是有了云计算、大数据,集群规模非常大,而且都要求在一个二层网络里面。这就需要二层互连从汇聚层上升为核心层,也即在核心以下,全部是二层互连,全部在一个广播域里面,这就是常说的大二层。

如果大二层横向流量不大,核心交换机数目不多,可以做堆叠,但是如果横向流量很大,仅仅堆叠满足不了,就需要部署多组核心交换机,而且要和汇聚层进行全互连。由于堆叠只解决一个核心交换机组内的无环问题,而组之间全互连,还需要其他机制进行解决。

如果是STP,那部署多组核心无法扩大横向流量的能力,因为还是只有一组起作用。

于是大二层就引入了TRILL(Transparent Interconnection of Lots of Link),即多链接透明互联协议。它的基本思想是,二层环有问题,三层环没有问题,那就把三层的路由能力模拟在二层实现。

运行TRILL协议的交换机称为RBridge,是具有路由转发特性的网桥设备,只不过这个路由是根据MAC地址来的,不是根据IP来的。

Rbridage之间通过链路状态协议运作。记得这个路由协议吗?通过它可以学习整个大二层的拓扑,知道访问哪个MAC应该从哪个网桥走;还可以计算最短的路径,也可以通过等价的路由进行负载均衡和高可用性。

在核心交换上面,往往会挂一些安全设备,例如入侵检测、DDoS防护等等。这是整个数据中心的屏障,防止来自外来的攻击。核心交换机上往往还有负载均衡器,原理前面的章节已经说过了。

在有的数据中心里面,对于存储设备,还会有一个存储网络,用来连接SAN和NAS。但是对于新的云计算来讲,往往不使用传统的SAN和NAS,而使用部署在x86机器上的软件定义存储,这样存储也是服务器了,而且可以和计算节点融合在一个机架上,从而更加有效率,也就没有了单独的存储网络了。

这是一个典型的三层网络结构。这里的三层不是指IP层,而是指接入层、汇聚层、核心层三层。这种模式非常有利于外部流量请求到内部应用。这个类型的流量,是从外到内或者从内到外,对应到上面那张图里,就是从上到下,从下到上,上北下南,所以称为南北流量。

但是随着云计算和大数据的发展,节点之间的交互越来越多,例如大数据计算经常要在不同的节点将数据拷贝来拷贝去,这样需要经过交换机,使得数据从左到右,从右到左,左西右东,所以称为东西流量。

为了解决东西流量的问题,演进出了叶脊网络(Spine/Leaf)。

叶子交换机(leaf),直接连接物理服务器。L2/L3网络的分界点在叶子交换机上,叶子交换机之上是三层网络。

脊交换机(spine switch),相当于核心交换机。叶脊之间通过ECMP动态选择多条路径。脊交换机现在只是为叶子交换机提供一个弹性的L3路由网络。南北流量可以不用直接从脊交换机发出,而是通过与leaf交换机并行的交换机,再接到边界路由器出去。

传统的三层网络架构是垂直的结构,而叶脊网络架构是扁平的结构,更易于水平扩展。

数据中心分为三层。服务器连接到接入层,然后是汇聚层,再然后是核心层,最外面是边界路由器和安全设备。

数据中心的所有链路都需要高可用性。服务器需要绑定网卡,交换机需要堆叠,三层设备可以通过等价路由,二层设备可以通过TRILL协议。

随着云和大数据的发展,东西流量相对于南北流量越来越重要,因而演化为叶脊网络结构。

第一种方式是走公网,但是公网太不安全,你的隐私可能会被别人偷窥。

第二种方式是租用专线的方式把它们连起来,这是土豪的做法,需要花很多钱。

第三种方式是用VPN来连接,这种方法比较折中,安全又不贵。

VPN,全名Virtual Private Network,虚拟专用网,就是利用开放的公众网络,建立专用数据传输通道,将远程的分支机构、移动办公人员等连接起来。

VPN通过隧道技术在公众网络上仿真一条点到点的专线,是通过利用一种协议来传输另外一种协议的技术,这里面涉及三种协议:乘客协议、隧道协议和承载协议。

你知道如何通过自驾进行海南游吗?这其中,你的车怎么通过琼州海峡呢?这里用到轮渡,其实这就用到隧道协议。

在广州这边开车是有“协议”的,例如靠右行驶、红灯停、绿灯行,这个就相当于“被封装”的乘客协议。当然在海南那面,开车也是同样的协议。这就相当于需要连接在一起的一个公司的两个分部。

但是在海上坐船航行,也有它的协议,例如要看灯塔、要按航道航行等。这就是外层的承载协议。

那我的车如何从广州到海南呢?这就需要你遵循开车的协议,将车开上轮渡,所有通过轮渡的车都关在船舱里面,按照既定的规则排列好,这就是隧道协议。

在大海上,你的车是关在船舱里面的,就像在隧道里面一样,这个时候内部的乘客协议,也即驾驶协议没啥用处,只需要船遵从外层的承载协议,到达海南就可以了。

到达之后,外部承载协议的任务就结束了,打开船舱,将车开出来,就相当于取下承载协议和隧道协议的头。接下来,在海南该怎么开车,就怎么开车,还是内部的乘客协议起作用。

在最前面的时候说了,直接使用公网太不安全,所以接下来我们来看一种十分安全的VPN,IPsec VPN。这是基于IP协议的安全隧道协议,为了保证在公网上面信息的安全,因而采取了一定的机制保证安全性。

机制一:私密性,防止信息泄漏给未经授权的个人,通过加密把数据从明文变成无法读懂的密文,从而确保数据的私密性。
前面讲HTTPS的时候,说过加密可以分为对称加密和非对称加密。对称加密速度快一些。而VPN一旦建立,需要传输大量数据,因而我们采取对称加密。但是同样,对称加密还是存在加密秘钥如何传输的问题,这里需要用到因特网密钥交换(IKE,Internet Key Exchange)协议。

机制二:完整性,数据没有被非法篡改,通过对数据进行hash运算,产生类似于指纹的数据摘要,以保证数据的完整性。

机制三:真实性,数据确实是由特定的对端发出,通过身份认证可以保证数据的真实性。

那如何保证对方就是真正的那个人呢?

第一种方法就是预共享密钥,也就是双方事先商量好一个暗号,比如“天王盖地虎,宝塔镇河妖”,对上了,就说明是对的。

另外一种方法就是用数字签名来验证。咋签名呢?当然是使用私钥进行签名,私钥只有我自己有,所以如果对方能用我的数字证书里面的公钥解开,就说明我是我。

基于以上三个特性,组成了IPsec VPN的协议簇。这个协议簇内容比较丰富。

在这个协议簇里面,有两种协议,这两种协议的区别在于封装网络包的格式不一样。

一种协议称为AH(Authentication Header),只能进行数据摘要 ,不能实现数据加密。

还有一种ESP(Encapsulating Security Payload),能够进行数据加密和数据摘要。

在这个协议簇里面,还有两类算法,分别是加密算法和摘要算法。

这个协议簇还包含两大组件,一个用于VPN的双方要进行对称密钥的交换的IKE组件,另一个是VPN的双方要对连接进行维护的SA(Security Association)组件。

IPsec VPN的建立过程
下面来看IPsec VPN的建立过程,这个过程分两个阶段。

第一个阶段,建立IKE自己的SA。这个SA用来维护一个通过身份认证和安全保护的通道,为第二个阶段提供服务。在这个阶段,通过DH(Diffie-Hellman)算法计算出一个对称密钥K。

DH算法是一个比较巧妙的算法。客户端和服务端约定两个公开的质数p和q,然后客户端随机产生一个数a作为自己的私钥,服务端随机产生一个b作为自己的私钥,客户端可以根据p、q和a计算出公钥A,服务端根据p、q和b计算出公钥B,然后双方交换公钥A和B。

到此客户端和服务端可以根据已有的信息,各自独立算出相同的结果K,就是对称密钥。但是这个过程,对称密钥从来没有在通道上传输过,只传输了生成密钥的材料,通过这些材料,截获的人是无法算出的。

有了这个对称密钥K,接下来是第二个阶段,建立IPsec SA。在这个SA里面,双方会生成一个随机的对称密钥M,由K加密传给对方,然后使用M进行双方接下来通信的数据。对称密钥M是有过期时间的,会过一段时间,重新生成一次,从而防止被破解。

IPsec SA里面有以下内容:

SPI(Security Parameter Index),用于标识不同的连接;

双方商量好的加密算法、哈希算法和封装模式;

生存周期,超过这个周期,就需要重新生成一个IPsec SA,重新生成对称密钥。

这种点对点的基于IP的VPN,能满足互通的要求,但是速度往往比较慢,这是由底层IP协议的特性决定的。IP不是面向连接的,是尽力而为的协议,每个IP包自由选择路径,到每一个路由器,都自己去找下一跳,丢了就丢了,是靠上一层TCP的重发来保证可靠性。

因为IP网络从设计的时候,就认为是不可靠的,所以即使同一个连接,也可能选择不同的道路,这样的好处是,一条道路崩溃的时候,总有其他的路可以走。当然,带来的代价就是,不断的路由查找,效率比较差。

和IP对应的另一种技术称为ATM。这种协议和IP协议的不同在于,它是面向连接的。你可以说TCP也是面向连接的啊。这两个不同,ATM和IP是一个层次的,和TCP不是一个层次的。

在MPLS头里面,首先是标签值占20位,接着是3位实验位,再接下来是1位栈底标志位,表示当前标签是否位于栈底了。这样就允许多个标签被编码到同一个数据包中,形成标签栈。最后是8位TTL存活时间字段,如果标签数据包的出发TTL值为0,那么该数据包在网络中的生命期被认为已经过期了。

有了标签,还需要设备认这个标签,并且能够根据这个标签转发,这种能够转发标签的路由器称为标签交换路由器(LSR,Label Switching Router)。

这种路由器会有两个表格,一个就是传统的FIB,也即路由表,另一个就是LFIB,标签转发表。有了这两个表,既可以进行普通的路由转发,也可以进行基于标签的转发。

另外,TCP所谓的面向连接,是不停地重试来保证成功,其实下层的IP还是不面向连接的,丢了就丢了。ATM是传输之前先建立一个连接,形成一个虚拟的通路,一旦连接建立了,所有的包都按照相同的路径走,不会分头行事。

好处是不需要每次都查路由表的,虚拟路径已经建立,打上了标签,后续的包傻傻的跟着走就是了,不用像IP包一样,每个包都思考下一步怎么走,都按相同的路径走,这样效率会高很多。

但是一旦虚拟路径上的某个路由器坏了,则这个连接就断了,什么也发不过去了,因为其他的包还会按照原来的路径走,都掉坑里了,它们不会选择其他的路径走。

ATM技术虽然没有成功,但其屏弃了繁琐的路由查找,改为简单快速的标签交换,将具有全局意义的路由表改为只有本地意义的标签表,这些都可以大大提高一台路由器的转发功力。

有没有一种方式将两者的优点结合起来呢?这就是多协议标签交换(MPLS,Multi-Protocol Label Switching)。MPLS的格式如图所示,在原始的IP头之外,多了MPLS的头,里面可以打标签。

这里我们区分MPLS区域和非MPLS区域。在MPLS区域中间,使用标签进行转发,非MPLS区域,使用普通路由转发,在边缘节点上,需要有能力将对于普通路由的转发,变成对于标签的转发。

例如图中要访问114.1.1.1,在边界上查找普通路由,发现马上要进入MPLS区域了,进去了对应标签1,于是在IP头外面加一个标签1,在区域里面,标签1要变成标签3,标签3到达出口边缘,将标签去掉,按照路由发出。

这样一个通过标签转换而建立的路径称为LSP,标签交换路径。在一条LSP上,沿数据包传送的方向,相邻的LSR分别叫上游LSR(upstream LSR)和下游LSR(downstream LSR)。

有了标签,转发是很简单的事,但是如何生成标签,却是MPLS中最难修炼的部分。在MPLS秘笈中,这部分被称为LDP(Label Distribution Protocol),是一个动态的生成标签的协议。

其实LDP与IP帮派中的路由协议十分相像,通过LSR的交互,互相告知去哪里应该打哪个标签,称为标签分发,往往是从下游开始的。

如果有一个边缘节点发现自己的路由表中出现了新的目的地址,它就要给别人说,我能到达一条新的路径了。

如果此边缘节点存在上游LSR,并且尚有可供分配的标签,则该节点为新的路径分配标签,并向上游发出标签映射消息,其中包含分配的标签等信息。

收到标签映射消息的LSR记录相应的标签映射信息,在其标签转发表中增加相应的条目。此LSR为它的上游LSR分配标签,并继续向上游LSR发送标签映射消息。

当入口LSR收到标签映射消息时,在标签转发表中增加相应的条目。这时,就完成了LSP的建立。有了标签,转发轻松多了,但是这个和VPN什么关系呢?

可以想象,如果我们VPN通道里面包的转发,都是通过标签的方式进行,效率就会高很多。所以要想个办法把MPLS应用于VPN。

在MPLS VPN中,网络中的路由器分成以下几类:

PE(Provider Edge):运营商网络与客户网络相连的边缘网络设备;

CE(Customer Edge):客户网络与PE相连接的边缘设备;

P(Provider):这里特指运营商网络中除PE之外的其他运营商网络设备。

为什么要这样分呢?因为我们发现,在运营商网络里面,也即P Router之间,使用标签是没有问题的,因为都在运营商的管控之下,对于网段,路由都可以自己控制。但是一旦客户要接入这个网络,就复杂得多。

首先是客户地址重复的问题。客户所使用的大多数都是私网的地址(192.168.X.X;10.X.X.X;172.X.X.X),而且很多情况下都会与其它的客户重复。

比如,机构A和机构B都使用了192.168.101.0/24网段的地址,这就发生了地址空间重叠(Overlapping Address Spaces)。

首先困惑的是BGP协议,既然VPN将两个数据中心连起来,应该看起来像一个数据中心一样,那么如何到达另一端需要通过BGP将路由广播过去,传统BGP无法正确处理地址空间重叠的VPN的路由。

假设机构A和机构B都使用了192.168.101.0/24网段的地址,并各自发布了一条去往此网段的路由,BGP将只会选择其中一条路由,从而导致去往另一个VPN的路由丢失。

所以PE路由器之间使用特殊的MP-BGP来发布VPN路由,在相互沟通的消息中,在一般32位IPv4的地址之前加上一个客户标示的区分符用于客户地址的区分,这种称为VPN-IPv4地址族,这样PE路由器会收到如下的消息,机构A的192.168.101.0/24应该往这面走,机构B的192.168.101.0/24则应该去另外一个方向。

另外困惑的是路由表,当两个客户的IP包到达PE的时候,PE就困惑了,因为网段是重复的。

如何区分哪些路由是属于哪些客户VPN内的?如何保证VPN业务路由与普通路由不相互干扰?

在PE上,可以通过VRF(VPN Routing&Forwarding Instance)建立每个客户一个路由表,与其它VPN客户路由和普通路由相互区分。可以理解为专属于客户的小路由器。

远端PE通过MP-BGP协议把业务路由放到近端PE,近端PE根据不同的客户选择出相关客户的业务路由放到相应的VRF路由表中。

VPN报文转发采用两层标签方式:

第一层(外层)标签在骨干网内部进行交换,指示从PE到对端PE的一条LSP。VPN报文利用这层标签,可以沿LSP到达对端PE;

第二层(内层)标签在从对端PE到达CE时使用,在PE上,通过查找VRF表项,指示报文应被送到哪个VPN用户,或者更具体一些,到达哪一个CE。这样,对端PE根据内层标签可以找到转发报文的接口。

VPN可以将一个机构的多个数据中心通过隧道的方式连接起来,让机构感觉在一个数据中心里面,就像自驾游通过琼州海峡一样;

完全基于软件的IPsec VPN可以保证私密性、完整性、真实性、简单便宜,但是性能稍微差一些;

MPLS-VPN综合和IP转发模式和ATM的标签转发模式的优势,性能较好,但是需要从运营商购买。

2G网络
手机本来是用来打电话的,不是用来上网的,所以原来在2G时代,上网使用的不是IP网络,而是电话网络,走模拟信号,专业名称为公共交换电话网(PSTN,Public Switched Telephone Network)。

那手机不连网线,也不连电话线,它是怎么上网的呢?

手机是通过收发无线信号来通信的,专业名称是Mobile Station,简称MS,需要嵌入SIM。手机是客户端,而无线信号的服务端,就是基站子系统(BSS,Base Station SubsystemBSS)。至于什么是基站,你可以回想一下,你在爬山的时候,是不是看到过信号塔?我们平时城市里面的基站比较隐蔽,不容易看到,所以只有在山里才会注意到。正是这个信号塔,通过无线信号,让你的手机可以进行通信。

但是你要知道一点,无论无线通信如何无线,最终还是要连接到有线的网络里。前面讲数据中心的时候我也讲过,电商的应用是放在数据中心的,数据中心的电脑都是插着网线的。

因而,基站子系统分两部分,一部分对外提供无线通信,叫作基站收发信台(BTS,Base Transceiver Station),另一部分对内连接有线网络,叫作基站控制器(BSC,Base Station Controller)。基站收发信台通过无线收到数据后,转发给基站控制器。

这部分属于无线的部分,统称为无线接入网(RAN,Radio Access Network)。

基站控制器通过有线网络,连接到提供手机业务的运营商的数据中心,这部分称为核心网(CN,Core Network)。核心网还没有真的进入互联网,这部分还是主要提供手机业务,是手机业务的有线部分。

首先接待基站来的数据的是移动业务交换中心(MSC,Mobile Service Switching Center),它是进入核心网的入口,但是它不会让你直接连接到互联网上。

因为在让你的手机真正进入互联网之前,提供手机业务的运营商,需要认证是不是合法的手机接入。别你自己造了一张手机卡,就连接上来。鉴权中心(AUC,Authentication Center)和设备识别寄存器(EIR,Equipment Identity Register)主要是负责安全性的。

另外,需要看你是本地的号,还是外地的号,这个牵扯到计费的问题,异地收费还是很贵的。访问位置寄存器(VLR,Visit Location Register)是看你目前在的地方,归属位置寄存器(HLR,Home Location Register)是看你的号码归属地。

当你的手机卡既合法又有钱的时候,才允许你上网,这个时候需要一个网关,连接核心网和真正的互联网。网关移动交换中心(GMSC ,Gateway Mobile Switching Center)就是干这个的,然后是真正的互连网。在2G时代,还是电话网络PSTN。

数据中心里面的这些模块统称为网络子系统(NSS,Network and Switching Subsystem)。

因而2G时代的上网如图所示,我们总结一下,有这几个核心点:

手机通过无线信号连接基站;

基站一面朝前接无线,一面朝后接核心网;

核心网一面朝前接到基站请求,一是判断你是否合法,二是判断你是不是本地号,还有没有钱,一面通过网关连接电话网络。

2.5G网络
后来从2G到了2.5G,也即在原来电路交换的基础上,加入了分组交换业务,支持Packet的转发,从而支持IP网络。

在上述网络的基础上,基站一面朝前接无线,一面朝后接核心网。在朝后的组件中,多了一个分组控制单元(PCU,Packet Control Unit),用以提供分组交换通道。

在核心网里面,有个朝前的接待员(SGSN,Service GPRS Supported Node)和朝后连接IP网络的网关型GPRS支持节点(GGSN,Gateway GPRS Supported Node)。

3G网络
到了3G时代,主要是无线通信技术有了改进,大大增加了无线的带宽。

以W-CDMA为例,理论最高2M的下行速度,因而基站改变了,一面朝外的是Node B,一面朝内连接核心网的是无线网络控制器(RNC,Radio Network Controller)。核心网以及连接的IP网络没有什么变化。

4G网络
然后就到了今天的4G网络,基站为eNodeB,包含了原来Node B和RNC的功能,下行速度向百兆级别迈进。另外,核心网实现了控制面和数据面的分离,这个怎么理解呢?

在前面的核心网里面,有接待员MSC或者SGSN,你会发现检查是否合法是它负责,转发数据也是它负责,也即控制面和数据面是合二为一的,这样灵活性比较差,因为控制面主要是指令,多是小包,往往需要高的及时性;数据面主要是流量,多是大包,往往需要吞吐量。

HSS用于存储用户签约信息的数据库,其实就是你这个号码归属地是哪里的,以及一些认证信息。

MME是核心控制网元,是控制面的核心,当手机通过eNodeB连上的时候,MME会根据HSS的信息,判断你是否合法。如果允许连上来,MME不负责具体的数据的流量,而是MME会选择数据面的SGW和PGW,然后告诉eNodeB,我允许你连上来了,你连接它们吧。

于是手机直接通过eNodeB连接SGW,连上核心网,SGW相当于数据面的接待员,并通过PGW连到IP网络。PGW就是出口网关。在出口网关,有一个组件PCRF,称为策略和计费控制单元,用来控制上网策略和流量的计费。

UE就是你的手机,eNodeB还是两面派,朝前对接无线网络,朝后对接核心网络,在控制面对接的是MME。

eNodeB和MME之间的连接就是很正常的IP网络,但是这里面在IP层之上,却既不是TCP,也不是UDP,而是SCTP。这也是传输层的协议,也是面向连接的,但是更加适合移动网络。 它继承了TCP较为完善的拥塞控制并改进TCP的一些不足之处。

SCTP的第一个特点是多宿主。一台机器可以有多个网卡,而对于TCP连接来讲,虽然服务端可以监听0.0.0.0,也就是从哪个网卡来的连接都能接受,但是一旦建立了连接,就建立了四元组,也就选定了某个网卡。

SCTP引入了联合(association)的概念,将多个接口、多条路径放到一个联合中来。当检测到一条路径失效时,协议就会通过另外一条路径来发送通信数据。应用程序甚至都不必知道发生了故障、恢复,从而提供更高的可用性和可靠性。

SCTP的第二个特点是将一个联合分成多个流。一个联合中的所有流都是独立的,但均与该联合相关。每个流都给定了一个流编号,它被编码到SCTP报文中,通过联合在网络上传送。在TCP的机制中,由于强制顺序,导致前一个不到达,后一个就得等待,SCTP的多个流不会相互阻塞。

SCTP的第三个特点是四次握手,防止SYN攻击。在TCP中是三次握手,当服务端收到客户的SYN之后,返回一个SYN-ACK之前,就建立数据结构,并记录下状态,等待客户端发送ACK的ACK。当恶意客户端使用虚假的源地址来伪造大量SYN报文时,服务端需要分配大量的资源,最终耗尽资源,无法处理新的请求。

SCTP可以通过四次握手引入Cookie的概念,来有效地防止这种攻击的产生。在SCTP中,客户机使用一个INIT报文发起一个连接。服务器使用一个INIT-ACK报文进行响应,其中就包括了Cookie。然后客户端就使用一个COOKIE-ECHO报文进行响应,其中包含了服务器所发送的Cookie。这个时候,服务器为这个连接分配资源,并通过向客户机发送一个COOKIE-ACK报文对其进行响应。

SCTP的第四个特点是将消息分帧。TCP是面向流的,也即发送的数据没头没尾,没有明显的界限。这对于发送数据没有问题,但是对于发送一个个消息类型的数据,就不太方便。有可能客户端写入10个字节,然后再写入20个字节。服务端不是读出10个字节的一个消息,再读出20个字节的一个消息,而有可能读入25个字节,再读入5个字节,需要业务层去组合成消息。

SCTP借鉴了UDP的机制,在数据传输中提供了消息分帧功能。当一端对一个套接字执行写操作时,可确保对等端读出的数据大小与此相同。

SCTP的第五个特点是断开连接是三次挥手。在TCP里面,断开连接是四次挥手,允许另一端处于半关闭的状态。SCTP选择放弃这种状态,当一端关闭自己的套接字时,对等的两端全部需要关闭,将来任何一端都不允许再进行数据的移动了。

当MME通过认证鉴权,同意这个手机上网的时候,需要建立一个数据面的数据通路。建立通路的过程还是控制面的事情,因而使用的是控制面的协议GTP-C。

建设的数据通路分两段路,其实是两个隧道。一段是从eNodeB到SGW,这个数据通路由MME通过S1-MME协议告诉eNodeB,它是隧道的一端,通过S11告诉SGW,它是隧道的另一端。第二端是从SGW到PGW,SGW通过S11协议知道自己是其中一端,并主动通过S5协议,告诉PGW它是隧道的另一端。

手机开机以后,在附近寻找基站eNodeB,找到后给eNodeB发送Attach Request,说“我来啦,我要上网”。

eNodeB将请求发给MME,说“有个手机要上网”。

MME去请求手机,一是认证,二是鉴权,还会请求HSS看看有没有钱,看看是在哪里上网。

当MME通过了手机的认证之后,开始分配隧道,先告诉SGW,说要创建一个会话(Create Session)。在这里面,会给SGW分配一个隧道ID t1,并且请求SGW给自己也分配一个隧道ID。

SGW转头向PGW请求建立一个会话,为PGW的控制面分配一个隧道ID t2,也给PGW的数据面分配一个隧道ID t3,并且请求PGW给自己的控制面和数据面分配隧道ID。

PGW回复SGW说“创建会话成功”,使用自己的控制面隧道ID t2,回复里面携带着给SGW控制面分配的隧道ID t4和控制面的隧道ID t5,至此SGW和PGW直接的隧道建设完成。双方请求对方,都要带着对方给自己分配的隧道ID,从而标志是这个手机的请求。

接下来SGW回复MME说“创建会话成功”,使用自己的隧道ID t1访问MME,回复里面有给MME分配隧道ID t6,也有SGW给eNodeB分配的隧道ID t7。

当MME发现后面的隧道都建设成功之后,就告诉eNodeB,“后面的隧道已经建设完毕,SGW给你分配的隧道ID是t7,你可以开始连上来了,但是你也要给SGW分配一个隧道ID”。

eNodeB告诉MME自己给SGW分配一个隧道,ID为t8。

MME将eNodeB给SGW分配的隧道ID t8告知SGW,从而前面的隧道也建设完毕。

这样,手机就可以通过建立的隧道成功上网了。

移动网络的发展历程从2G到3G,再到4G,逐渐从打电话的功能为主,向上网的功能为主转变;

请记住4G网络的结构,有eNodeB、MME、SGW、PGW等,分控制面协议和数据面协议,你可以对照着结构,试着说出手机上网的流程;

即便你在国外的运营商下上网,也是要通过国内运营商控制的,因而也上不了脸书。

在数据中心里面,也有一种类似的开源技术qemu-kvm,能让你在一台巨大的物理机里面,掏出一台台小的机器。这套软件就能解决上面的问题:一点就能创建,一点就能销毁。你想要多大就有多大,每次创建的系统还都是新的。

我们常把物理机比喻为自己拿地盖房子,而虚拟机则相当于购买公寓,更加灵活方面,随时可买可卖。 那这个软件为什么能做到这些事儿呢?

它用的是软件模拟硬件的方式。刚才说了,数据中心里面用的qemu-kvm。从名字上来讲,emu就是Emulator(模拟器)的意思,主要会模拟CPU、内存、网络、硬盘,使得虚拟机感觉自己在使用独立的设备,但是真正使用的时候,当然还是使用物理的设备。

例如,多个虚拟机轮流使用物理CPU,内存也是使用虚拟内存映射的方式,最终映射到物理内存上。硬盘在一块大的文件系统上创建一个N个G的文件,作为虚拟机的硬盘。

简单比喻,虚拟化软件就像一个“骗子”,向上“骗”虚拟机里面的应用,让它们感觉独享资源,其实自己啥都没有,全部向下从物理机里面弄。

虚拟机是物理机上跑着的一个软件。这个软件可以像其他应用打开文件一样,打开一个称为TUN/TAP的Char Dev(字符设备文件)。打开了这个字符设备文件之后,在物理机上就能看到一张虚拟TAP网卡。

虚拟化软件作为“骗子”,会将打开的这个文件,在虚拟机里面虚拟出一张网卡,让虚拟机里面的应用觉得它们真有一张网卡。于是,所有的网络包都往这里发。

当然,网络包会到虚拟化软件这里。它会将网络包转换成为文件流,写入字符设备,就像写一个文件一样。内核中TUN/TAP字符设备驱动会收到这个写入的文件流,交给TUN/TAP的虚拟网卡驱动。这个驱动将文件流再次转成网络包,交给TCP/IP协议栈,最终从虚拟TAP网卡发出来,成为标准的网络包。

就这样,几经转手,数据终于从虚拟机里面,发到了虚拟机外面。

虚拟网卡连接到云中

我们就这样有了虚拟TAP网卡。接下来就要看,这个卡怎么接入庞大的数据中心网络中。

在接入之前,我们先来看,云计算中的网络都需要注意哪些点。

  • 共享:尽管每个虚拟机都会有一个或者多个虚拟网卡,但是物理机上可能只有有限的网卡。那这么多虚拟网卡如何共享同一个出口?
  • 隔离:分两个方面,一个是安全隔离,两个虚拟机可能属于两个用户,那怎么保证一个用户的数据不被另一个用户窃听?一个是流量隔离,两个虚拟机,如果有一个疯狂下片,会不会导致另外一个上不了网?
  • 互通:分两个方面,一个是如果同一台机器上的两个虚拟机,属于同一个用户的话,这两个如何相互通信?另一个是如果不同物理机上的两个虚拟机,属于同一个用户的话,这两个如何相互通信?
  • 灵活:虚拟机和物理不同,会经常创建、删除,从一个机器漂移到另一台机器,有的互通、有的不通等等,灵活性比物理网络要好得多,需要能够灵活配置。

共享与互通问题

这些问题,我们一个个来解决。

首先,一台物理机上有多个虚拟机,有多个虚拟网卡,这些虚拟网卡如何连在一起,进行相互访问,并且可以访问外网呢?

还记得我们在大学宿舍里做的事情吗?你可以想象你的物理机就是你们宿舍,虚拟机就是你的个人电脑,这些电脑应该怎么连接起来呢?当然应该买一个交换机。

在物理机上,应该有一个虚拟的交换机,在Linux上有一个命令叫作brctl,可以创建虚拟的网桥brctl addbr br0。创建出来以后,将两个虚拟机的虚拟网卡,都连接到虚拟网桥brctl addif br0 tap0上,这样将两个虚拟机配置相同的子网网段,两台虚拟机就能够相互通信了。

如果要访问外部,往往有两种方式。

一种方式称为桥接。如果在桌面虚拟化软件上选择桥接网络,则在你的笔记本电脑上,就会形成下面的结构。

每个虚拟机都会有虚拟网卡,在你的笔记本电脑上,会发现多了几个网卡,其实是虚拟交换机。这个虚拟交换机将虚拟机连接在一起。在桥接模式下,物理网卡也连接到这个虚拟交换机上,物理网卡在桌面虚拟化软件上,在“界面名称”那里选定。

如果使用桥接网络,当你登录虚拟机里看IP地址的时候会发现,你的虚拟机的地址和你的笔记本电脑的,以及你旁边的同事的电脑的网段是一个网段。这是为什么呢?这其实相当于将物理机和虚拟机放在同一个网桥上,相当于这个网桥上有三台机器,是一个网段的,全部打平了。我将图画成下面的样子你就好理解了。

  • 云计算的关键技术是虚拟化,这里我们重点关注的是,虚拟网卡通过打开TUN/TAP字符设备的方式,将虚拟机内外连接起来;
  • 云中的网络重点关注四个方面,共享、隔离、互通、灵活。其中共享和互通有两种常用的方式,分别是桥接和NAT,隔离可以通过VLAN的方式。

软件定义网络(SDN)

这种模式就像传统的网络设备和普通的Linux网桥的模式,配置整个云平台的网络通路,你需要登录到这台机器上配置这个,再登录到另外一个设备配置那个,才能成功。

如果物业管理人员有一套智能的控制系统,在物业监控室里就能看到小区里每个单元、每个电梯的人流情况,然后在监控室里面,只要通过远程控制的方式,拨弄一个手柄,电梯的速度就调整了,栅栏门就打开了,某个入口就改出口了。

这就是软件定义网络(SDN)。它主要有以下三个特点。

  • 控制与转发分离:转发平面就是一个个虚拟或者物理的网络设备,就像小区里面的一条条路。控制平面就是统一的控制中心,就像小区物业的监控室。它们原来是一起的,物业管理员要从监控室出来,到路上去管理设备,现在是分离的,路就是走人的,控制都在监控室。
  • 控制平面与转发平面之间的开放接口:控制器向上提供接口,被应用层调用,就像总控室提供按钮,让物业管理员使用。控制器向下调用接口,来控制网络设备,就像总控室会远程控制电梯的速度。这里经常使用两个名词,前面这个接口称为北向接口,后面这个接口称为南向接口,上北下南嘛。
  • 逻辑上的集中控制:逻辑上集中的控制平面可以控制多个转发面设备,也就是控制整个物理网络,因而可以获得全局的网络状态视图,并根据该全局网络状态视图实现对网络的优化控制,就像物业管理员在监控室能够看到整个小区的情况,并根据情况优化出入方案。

OpenFlow和OpenvSwitch

SDN有很多种实现方式,我们来看一种开源的实现方式。

OpenFlow是SDN控制器和网络设备之间互通的南向接口协议,OpenvSwitch用于创建软件的虚拟交换机。OpenvSwitch是支持OpenFlow协议的,当然也有一些硬件交换机也支持OpenFlow协议。它们都可以被统一的SDN控制器管理,从而实现物理机和虚拟机的网络连通。

在OpenvSwitch里面,有一个流表规则,任何通过这个交换机的包,都会经过这些规则进行处理,从而接收、转发、放弃。

那流表长啥样呢?其实就是一个个表格,每个表格好多行,每行都是一条规则。每条规则都有优先级,先看高优先级的规则,再看低优先级的规则。

对于每一条规则,要看是否满足匹配条件。这些条件包括,从哪个端口进来的,网络包头里面有什么等等。满足了条件的网络包,就要执行一个动作,对这个网络包进行处理。可以修改包头里的内容,可以跳到任何一个表格,可以转发到某个网口出去,也可以丢弃。

通过这些表格,可以对收到的网络包随意处理。

具体都能做什么处理呢?通过上面的表格可以看出,简直是想怎么处理怎么处理,可以覆盖TCP/IP协议栈的四层。

对于物理层:

  • 匹配规则包括由从哪个口进来;
  • 执行动作包括从哪个口出去。

对于MAC层:

  • 匹配规则包括:源MAC地址是多少?(dl_src),目标MAC是多少?(dl_dst),所属vlan是多少?(dl_vlan);
  • 执行动作包括:修改源MAC(mod_dl_src),修改目标MAC(mod_dl_dst),修改VLAN(mod_vlan_vid),删除VLAN(strip_vlan),MAC地址学习(learn)。

对于网络层:

  • 匹配规则包括:源IP地址是多少?(nw_src),目标IP是多少?(nw_dst)。
  • 执行动作包括:修改源IP地址(mod_nw_src),修改目标IP地址(mod_nw_dst)。

对于传输层:

  • 匹配规则包括:源端口是多少?(tp_src),目标端口是多少?(tp_dst)。
  • 执行动作包括:修改源端口(mod_tp_src),修改目标端口(mod_tp_dst)。

总而言之,对于OpenvSwitch来讲,网络包到了我手里,就是一个Buffer,我想怎么改怎么改,想发到哪个端口就发送到哪个端口。

OpenvSwitch有本地的命令行可以进行配置,能够实验咱们前面讲过的一些功能。我们可以通过OpenvSwitch的命令创建一个虚拟交换机。然后可以将多个虚拟端口port添加到这个虚拟交换机上。

实验一:用OpenvSwitch实现VLAN的功能

下面我们实验一下通过OpenvSwitch实现VLAN的功能,在OpenvSwitch中端口port分两种。

第一类是access port:

  • 这个端口配置tag,从这个端口进来的包会被打上这个tag;
  • 如果网络包本身带有的VLAN ID等于tag,则会从这个port发出;
  • 从access port发出的包不带VLAN ID。

第二类是trunk port:

  • 这个port不配置tag,配置trunks;
  • 如果trunks为空,则所有的VLAN都trunk,也就意味着对于所有VLAN的包,本身带什么VLAN ID,就是携带者什么VLAN ID,如果没有设置VLAN,就属于VLAN 0,全部允许通过;
  • 如果trunks不为空,则仅仅带着这些VLAN ID的包通过。

OpenvSwitch包含很多的模块,在用户态有两个重要的进程,也有两个重要的命令行工具。

  • 第一个进程是OVSDB进程。ovs-vsctl命令行会和这个进程通信,去创建虚拟交换机,创建端口,将端口添加到虚拟交换机上,OVSDB会将这些拓扑信息保存在一个本地的文件中。
  • 第一个进程是vswitchd进程。ovs-ofctl命令行会和这个进程通信,去下发流表规则,规则里面会规定如何对网络包进行处理,vswitchd会将流表放在用户态Flow Table中。

在内核态,OpenvSwitch有内核模块OpenvSwitch.ko,对应图中的Datapath部分。在网卡上注册一个函数,每当有网络包到达网卡的时候,这个函数就会被调用。

在内核的这个函数里面,会拿到网络包,将各个层次的重要信息拿出来,例如:

  • 在物理层,in_port即包进入的网口的ID;
  • 在MAC层,源和目的MAC地址;
  • 在IP层,源和目的IP地址;
  • 在传输层,源和目的端口号。

在内核中,有一个内核态Flow Table。接下来内核模块在这个内核流表中匹配规则,如果匹配上了,则执行操作、修改包,或者转发或者放弃。如果内核没有匹配上,则需要进入用户态,用户态和内核态之间通过Linux的一个机制Netlink相互通信。

内核通过upcall,告知用户态进程vswitchd在用户态Flow Table里面去匹配规则,这里面的规则是全量的流表规则,而内核Flow Table里面的只是为了快速处理,保留了部分规则,内核里面的规则过一阵就会过期。

当在用户态匹配到了流表规则之后,就在用户态执行操作,同时将这个匹配成功的流表通过reinject下发到内核,从而接下来的包都能在内核找到这个规则。

这里调用openflow协议的,是本地的命令行工具,也可以是远程的SDN控制器,一个重要的SDN控制器是OpenDaylight。

在没有OpenvSwitch的时候,如果一个新的用户要使用一个新的VLAN,还需要创建一个属于新的VLAN的虚拟网卡,并且为这个租户创建一个单独的虚拟网桥,这样用户越来越多的时候,虚拟网卡和虚拟网桥会越来越多,管理非常复杂。

另一个问题是虚拟机的VLAN和物理环境的VLAN是透传的,也即从一开始规划的时候,就需要匹配起来,将物理环境和虚拟环境强绑定,本来就不灵活。

而引入了OpenvSwitch,状态就得到了改观。

首先,由于OpenvSwitch本身就是支持VLAN的,所有的虚拟机都可以放在一个网桥br0上,通过不同的用户配置不同的tag,就能够实现隔离。例如上面的图,用户A的虚拟机都在br0上,用户B的虚拟机都在br1上,有了OpenvSwitch,就可以都放在br0上,只是设置了不同的tag。

另外,还可以创建一个虚拟交换机br1,将物理网络和虚拟网络进行隔离。物理网络有物理网络的VLAN规划,虚拟机在一台物理机上,所有的VLAN都是从1开始的。由于一台机器上的虚拟机不会超过4096个,所以VLAN在一台物理机上如果从1开始,肯定够用了。

例如在图中,上面的物理机里面,用户A被分配的tag是1,用户B被分配的tag是2,而在下面的物理机里面,用户A被分配的tag是7,用户B被分配的tag是6。

如果物理机之间的通信和隔离还是通过VLAN的话,需要将虚拟机的VLAN和物理环境的VLAN对应起来,但为了灵活性,不一定一致,这样可以实现分别管理物理机的网络和虚拟机的网络。好在OpenvSwitch可以对包的内容进行修改。例如通过匹配dl_vlan,然后执行mod_vlan_vid来改进进出出物理机的网络包。

尽管租户多了,物理环境的VLAN还是不够用,但是有了OpenvSwitch的映射,将物理和虚拟解耦,从而可以让物理环境使用其他技术,而不影响虚拟机环境,这个我们后面再讲。

小结

好了,这一节就到这里了,我们来总结一下:

  • 用SDN控制整个云里面的网络,就像小区保安从总控室管理整个物业是一样的,将控制面和数据面进行了分离;
  • 一种开源的虚拟交换机的实现OpenvSwitch,它能对经过自己的包做任意修改,从而使得云对网络的控制十分灵活;
  • 将OpenvSwitch引入了云之后,可以使得配置简单而灵活,并且可以解耦物理网络和虚拟网络。

所以对于公有云上的虚拟机,我的建议是仅仅开放需要的端口,而将其他的端口一概关闭。这个时候,你只要通过安全措施守护好这个唯一的入口就可以了。采用的方式常常是用ACL(Access Control List,访问控制列表)来控制IP和端口。

设置好了这些规则,只有指定的IP段能够访问指定的开放接口,就算有个有漏洞的后台进程在那里,也会被屏蔽,黑客进不来。在云平台上,这些规则的集合常称为安全组。那安全组怎么实现呢?

首先拿下MAC头看看,是不是我的。如果是,则拿下IP头来。得到目标IP之后呢,就开始进行路由判断。在路由判断之前,这个节点我们称为PREROUTING。如果发现IP是我的,包就应该是我的,就发给上面的传输层,这个节点叫作INPUT。如果发现IP不是我的,就需要转发出去,这个节点称为FORWARD。如果是我的,上层处理完毕完毕后,一般会返回一个处理结果,这个处理结果会发出去,这个节点称为OUTPUT,无论是FORWARD还是OUTPUT,都是路由判断之后发生的,最后一个节点是POSTROUTING

整个包的处理过程还是原来的过程,只不过为什么要格外关注这五个节点呢?

是因为在Linux内核中,有一个框架叫Netfilter。它可以在这些节点插入hook函数。这些函数可以截获数据包,对数据包进行干预。例如做一定的修改,然后决策是否接着交给TCP/IP协议栈处理;或者可以交回给协议栈,那就是ACCEPT;或者过滤掉,不再传输,就是DROP;还有就是QUEUE,发送给某个用户态进程处理。

这个比较难理解,经常用在内部负载均衡,就是过来的数据一会儿传给目标地址1,一会儿传给目标地址2,而且目标地址的个数和权重都可能变。协议栈往往处理不了这么复杂的逻辑,需要写一个函数接管这个数据,实现自己的逻辑。

有了这个Netfilter框架就太好了,你可以在IP转发的过程中,随时干预这个过程,只要你能实现这些hook函数。

一个著名的实现,就是内核模块ip_tables。它在这五个节点上埋下函数,从而可以根据规则进行包的处理。按功能可分为四大类:连接跟踪(conntrack)、数据包的过滤(filter)、网络地址转换(nat)和数据包的修改(mangle)。其中连接跟踪是基础功能,被其他功能所依赖。其他三个可以实现包的过滤、修改和网络地址转换。

在用户态,还有一个你肯定知道的客户端程序iptables,用命令行来干预内核的规则。内核的功能对应iptables的命令行来讲,就是表和链的概念。

iptables的表分为四种:raw–>mangle–>nat–>filter。这四个优先级依次降低,raw不常用,所以主要功能都在其他三种表里实现。每个表可以设置多个链。

filter表处理过滤功能,主要包含三个链:

  • INPUT链:过滤所有目标地址是本机的数据包;
  • FORWARD链:过滤所有路过本机的数据包;
  • OUTPUT链:过滤所有由本机产生的数据包。

nat表主要是处理网络地址转换,可以进行Snat(改变数据包的源地址)、Dnat(改变数据包的目标地址),包含三个链:

  • PREROUTING链:可以在数据包到达防火墙时改变目标地址;
  • OUTPUT链:可以改变本地产生的数据包的目标地址;
  • POSTROUTING链:在数据包离开防火墙时改变数据包的源地址。

mangle表主要是修改数据包,包含:

  • PREROUTING链;
  • INPUT链;
  • FORWARD链;
  • OUTPUT链;
  • POSTROUTING链。

将iptables的表和链加入到上面的过程图中,就形成了下面的图和过程。

  1. 数据包进入的时候,先进mangle表的PREROUTING链。在这里可以根据需要,改变数据包头内容之后,进入nat表的PREROUTING链,在这里可以根据需要做Dnat,也就是目标地址转换。
  2. 进入路由判断,要判断是进入本地的还是转发的。
  3. 如果是进入本地的,就进入INPUT链,之后按条件过滤限制进入。
  4. 之后进入本机,再进入OUTPUT链,按条件过滤限制出去,离开本地。
  5. 如果是转发就进入FORWARD链,根据条件过滤限制转发。
  6. 之后进入POSTROUTING链,这里可以做Snat,离开网络接口。

有了iptables命令,我们就可以在云中实现一定的安全策略。例如我们可以处理前面的偷窥事件。首先我们将所有的门都关闭。

1
iptables -t filter -A INPUT -s 0.0.0.0/0.0.0.0 -d X.X.X.X -j DROP

-s表示源IP地址段,-d表示目标地址段,DROP表示丢弃,也即无论从哪里来的,要想访问我这台机器,全部拒绝,谁也黑不进来。

但是你发现坏了,ssh也进不来了,都不能远程运维了,可以打开一下。

1
iptables -I INPUT -s 0.0.0.0/0.0.0.0 -d X.X.X.X -p tcp --dport 22 -j ACCEPT

如果这台机器是提供的是web服务,80端口也应该打开,当然一旦打开,这个80端口就需要很好的防护,但是从规则角度还是要打开。

1
iptables -A INPUT -s 0.0.0.0/0.0.0.0 -d X.X.X.X -p tcp --dport 80 -j ACCEPT

这样就搞定了,其他的账户都封死,就一个防盗门可以进出,只要防盗门是五星级的,就比较安全了。

这些规则都可以在虚拟机里,自己安装iptables自己配置。但是如果虚拟机数目非常多,都要配置,对于用户来讲就太麻烦了,能不能让云平台把这部分工作做掉呢?

当然可以了。在云平台上,一般允许一个或者多个虚拟机属于某个安全组,而属于不同安全组的虚拟机之间的访问以及外网访问虚拟机,都需要通过安全组进行过滤。

为了方便运维,我们创建两个安全组,将Tomcat所在的虚拟机放在安全组A里面。在安全组A里面,允许任意IP地址0.0.0.0/0访问8080端口,但是对于ssh的22端口,仅仅允许管理员网段203.0.113.0/24访问。

我们将MySQL所在的虚拟机在安全组B里面。在安全组B里面,仅仅允许来自安全组A的机器访问3306端口,但是对于ssh的22端口,同样允许管理员网段203.0.113.0/24访问。

这些安全组规则都可以自动下发到每个在安全组里面的虚拟机上,从而控制一大批虚拟机的安全策略。这种批量下发是怎么做到的呢?你还记得这幅图吗?

两个VM都通过tap网卡连接到一个网桥上,但是网桥是二层的,两个VM之间是可以随意互通的,因而需要有一个地方统一配置这些iptables规则。

可以多加一个网桥,在这个网桥上配置iptables规则,将在用户在界面上配置的规则,放到这个网桥上。然后在每台机器上跑一个Agent,将用户配置的安全组变成iptables规则,配置在这个网桥上。

安全问题解决了,iptables真强大!别忙,iptables除了filter,还有nat呢,这个功能也非常重要。

前面的章节我们说过,在设计云平台的时候,我们想让虚拟机之间的网络和物理网络进行隔离,但是虚拟机毕竟还是要通过物理网和外界通信的,因而需要在出物理网的时候,做一次网络地址转换,也即nat,这个就可以用iptables来做。

我们学过,IP头里面包含源IP地址和目标IP地址,这两种IP地址都可以转换成其他地址。转换源IP地址的,我们称为Snat;转换目标IP地址的,我们称为Dnat。

你有没有思考过这个问题,TCP的访问都是一去一回的,而你在你家里连接WIFI的IP地址是一个私网IP,192.168.1.x。当你通过你们家的路由器访问163网站之后,网站的返回结果如何能够到达你的笔记本电脑呢?肯定不能通过192.168.1.x,这是个私网IP,不具有公网上的定位能力,而且用这个网段的人很多,茫茫人海,怎么能够找到你呢?

所以当你从你家里访问163网站的时候,在你路由器的出口,会做Snat的,运营商的出口也可能做Snat,将你的私网IP地址,最终转换为公网IP地址,然后163网站就可以通过这个公网IP地址返回结果,然后再nat回来,直到到达你的笔记本电脑。

云平台里面的虚拟机也是这样子的,它只有私网IP地址,到达外网网口要做一次Snat,转换成为机房网IP,然后出数据中心的时候,再转换为公网IP。

这里有一个问题是,在外网网口上做Snat的时候,是全部转换成一个机房网IP呢,还是每个虚拟机都对应一个机房网IP,最终对应一个公网IP呢?前面也说过了,公网IP非常贵,虚拟机也很多,当然不能每个都有单独的机房网和公网IP了,于是这种Snat是一种特殊的Snat,MASQUERADE(地址伪装)。

这种方式下,所有的虚拟机共享一个机房网和公网的IP地址,所有从外网网口出去的,都转换成为这个IP地址。那又一个问题来了,都变成一个公网IP了,当163网站返回结果的时候,给谁呢,再nat成为哪个私网的IP呢?

这就是Netfilter的连接跟踪(conntrack)功能了。对于TCP协议来讲,肯定是上来先建立一个连接,可以用“源/目的IP+源/目的端口”唯一标识一条连接,这个连接会放在conntrack表里面。当时是这台机器去请求163网站的,虽然源地址已经Snat成公网IP地址了,但是conntrack表里面还是有这个连接的记录的。当163网站返回数据的时候,会找到记录,从而找到正确的私网IP地址。

这是虚拟机做客户端的情况,如果虚拟机做服务器呢?也就是说,如果虚拟机里面部署的就是163网站呢?

这个时候就需要给这个网站配置固定的物理网的IP地址和公网IP地址了。这时候就需要显示的配置Snat规则和Dnat规则了。

当外部访问进来的时候,外网网口会通过Dnat规则将公网IP地址转换为私网IP地址,到达虚拟机,虚拟机里面是163网站,返回结果,外网网口会通过Snat规则,将私网IP地址转换为那个分配给它的固定的公网IP地址。

类似的规则如下:

  • 源地址转换(Snat):iptables -t nat -A -s 私网IP -j Snat –to-source 外网IP
  • 目的地址转换(Dnat):iptables -t nat -A -PREROUTING -d 外网IP -j Dnat –to-destination 私网IP

到此为止iptables解决了非法偷窥隐私的问题。

小结

好了,这一节就讲到这里了,我们来总结一下。

  • 云中的安全策略的常用方式是,使用iptables的规则,请记住它的五个阶段,PREROUTING、INPUT、FORWARD、OUTPUT、POSTROUTING。
  • iptables分为四种表,raw、mangle、nat、filter。其中安全策略主要在filter表中实现,而虚拟网络和物理网络地址的转换主要在nat表中实现。

在云平台上,也有这种现象,好在有一种流量控制的技术,可以实现QoS(Quality of Service),从而保障大多数用户的服务质量。

对于控制一台机器的网络的QoS,分两个方向,一个是入方向,一个是出方向。

其实我们能控制的只有出方向,通过Shaping,将出的流量控制成自己想要的模样。而进入的方向是无法控制的,只能通过Policy将包丢弃。

控制网络的QoS有哪些方式?

在Linux下,可以通过TC控制网络的QoS,主要就是通过队列的方式。

无类别排队规则

第一大类称为无类别排队规则(Classless Queuing Disciplines)。还记得我们讲ip addr的时候讲过的pfifo_fast,这是一种不把网络包分类的技术。

pfifo_fast分为三个先入先出的队列,称为三个Band。根据网络包里面TOS,看这个包到底应该进入哪个队列。TOS总共四位,每一位表示的意思不同,总共十六种类型。

通过命令行tc qdisc show dev eth0,可以输出结果priomap,也是十六个数字。在0到2之间,和TOS的十六种类型对应起来,表示不同的TOS对应的不同的队列。其中Band 0优先级最高,发送完毕后才轮到Band 1发送,最后才是Band 2。

另外一种无类别队列规则叫作随机公平队列(Stochastic Fair Queuing)。

会建立很多的FIFO的队列,TCP Session会计算hash值,通过hash值分配到某个队列。在队列的另一端,网络包会通过轮询策略从各个队列中取出发送。这样不会有一个Session占据所有的流量。

当然如果两个Session的hash是一样的,会共享一个队列,也有可能互相影响。hash函数会经常改变,从而session不会总是相互影响。

还有一种无类别队列规则称为令牌桶规则(TBF,Token Bucket Filte)。

所有的网络包排成队列进行发送,但不是到了队头就能发送,而是需要拿到令牌才能发送。

令牌根据设定的速度生成,所以即便队列很长,也是按照一定的速度进行发送的。

当没有包在队列中的时候,令牌还是以既定的速度生成,但是不是无限累积的,而是放满了桶为止。设置桶的大小为了避免下面的情况:当长时间没有网络包发送的时候,积累了大量的令牌,突然来了大量的网络包,每个都能得到令牌,造成瞬间流量大增。

基于类别的队列规则

另外一大类是基于类别的队列规则(Classful Queuing Disciplines),其中典型的为分层令牌桶规则HTB, Hierarchical Token Bucket)。

HTB往往是一棵树,接下来我举个具体的例子,通过TC如何构建一棵HTB树来带你理解。

使用TC可以为某个网卡eth0创建一个HTB的队列规则,需要付给它一个句柄为(1:)。

这是整棵树的根节点,接下来会有分支。例如图中有三个分支,句柄分别为(:10)、(:11)、(:12)。最后的参数default 12,表示默认发送给1:12,也即发送给第三个分支。

1
tc qdisc add dev eth0 root handle 1: htb default 12

对于这个网卡,需要规定发送的速度。一般有两个速度可以配置,一个是rate,表示一般情况下的速度;一个是ceil,表示最高情况下的速度。对于根节点来讲,这两个速度是一样的,于是创建一个root class,速度为(rate=100kbps,ceil=100kbps)。

1
tc class add dev eth0 parent 1: classid 1:1 htb rate 100kbps ceil 100kbps

接下来要创建分支,也即创建几个子class。每个子class统一有两个速度。三个分支分别为(rate=30kbps,ceil=100kbps)、(rate=10kbps,ceil=100kbps)、(rate=60kbps,ceil=100kbps)。

1
2
3
tc class add dev eth0 parent 1:1 classid 1:10 htb rate 30kbps ceil 100kbps
tc class add dev eth0 parent 1:1 classid 1:11 htb rate 10kbps ceil 100kbps
tc class add dev eth0 parent 1:1 classid 1:12 htb rate 60kbps ceil 100kbps

你会发现三个rate加起来,是整个网卡允许的最大速度。

HTB有个很好的特性,同一个root class下的子类可以相互借流量,如果不直接在队列规则下面创建一个root class,而是直接创建三个class,它们之间是不能相互借流量的。借流量的策略,可以使得当前不使用这个分支的流量的时候,可以借给另一个分支,从而不浪费带宽,使带宽发挥最大的作用。

最后,创建叶子队列规则,分别为fifosfq

1
2
3
tc qdisc add dev eth0 parent 1:10 handle 20: pfifo limit 5
tc qdisc add dev eth0 parent 1:11 handle 30: pfifo limit 5
tc qdisc add dev eth0 parent 1:12 handle 40: sfq perturb 10

基于这个队列规则,我们还可以通过TC设定发送规则:从1.2.3.4来的,发送给port 80的包,从第一个分支1:10走;其他从1.2.3.4发送来的包从第二个分支1:11走;其他的走默认分支。

1
2
tc filter add dev eth0 protocol ip parent 1:0 prio 1 u32 match ip src 1.2.3.4 match ip dport 80 0xffff flowid 1:10
tc filter add dev eth0 protocol ip parent 1:0 prio 1 u32 match ip src 1.2.3.4 flowid 1:11

如何控制QoS?

我们讲过,使用OpenvSwitch将云中的网卡连通在一起,那如何控制QoS呢?

就像我们上面说的一样,OpenvSwitch支持两种:

  • 对于进入的流量,可以设置策略Ingress policy;
1
2
ovs-vsctl set Interface tap0 ingress_policing_rate=100000
ovs-vsctl set Interface tap0 ingress_policing_burst=10000
  • 对于发出的流量,可以设置QoS规则Egress shaping,支持HTB。

我们构建一个拓扑图,来看看OpenvSwitch的QoS是如何工作的。

上面的命令创建了一个QoS规则,对应三个Queue。min-rate就是上面的rate,max-rate就是上面的ceil。通过交换机的网络包,要通过流表规则,匹配后进入不同的队列。然后我们就可以添加流表规则Flow(first_br是br0上的port 5)。

1
2
3
ovs-ofctl add-flow br0 "in_port=6 nw_src=192.168.100.100 actions=enqueue:5:0"
ovs-ofctl add-flow br0 "in_port=7 nw_src=192.168.100.101 actions=enqueue:5:1"
ovs-ofctl add-flow br0 "in_port=8 nw_src=192.168.100.102 actions=enqueue:5:2"

接下来,我们单独测试从192.168.100.100,192.168.100.101,192.168.100.102到192.168.100.103的带宽的时候,每个都是能够打满带宽的。

如果三个一起测试,一起狂发网络包,会发现是按照3:1:6的比例进行的,正是根据配置的队列的带宽比例分配的。

如果192.168.100.100和192.168.100.101一起测试,发现带宽占用比例为3:1,但是占满了总的流量,也即没有发包的192.168.100.102有60%的带宽被借用了。

如果192.168.100.100和192.168.100.102一起测试,发现带宽占用比例为1:2。如果192.168.100.101和192.168.100.102一起测试,发现带宽占用比例为1:6。

小结

好了,这一节就讲到这里了,我们来总结一下。

  • 云中的流量控制主要通过队列进行的,队列分为两大类:无类别队列规则和基于类别的队列规则。
  • 在云中网络Openvswitch中,主要使用的是分层令牌桶规则(HTB),将总的带宽在一棵树上按照配置的比例进行分配,并且在一个分支不用的时候,可以借给另外的分支,从而增强带宽利用率。

对于云平台中的隔离问题,前面咱们用的策略一直都是VLAN,但是我们也说过这种策略的问题,VLAN只有12位,共4096个。当时设计的时候,看起来是够了,但是现在绝对不够用,怎么办呢?

一种方式是修改这个协议。这种方法往往不可行,因为当这个协议形成一定标准后,千千万万设备上跑的程序都要按这个规则来。现在说改就放,谁去挨个儿告诉这些程序呢?很显然,这是一项不可能的工程。

另一种方式就是扩展,在原来包的格式的基础上扩展出一个头,里面包含足够用于区分租户的ID,外层的包的格式尽量和传统的一样,依然兼容原来的格式。一旦遇到需要区分用户的地方,我们就用这个特殊的程序,来处理这个特殊的包的格式。

这个概念很像咱们第22讲讲过的隧道理论,还记得自驾游通过摆渡轮到海南岛的那个故事吗?在那一节,我们说过,扩展的包头主要是用于加密的,而我们现在需要的包头是要能够区分用户的。

底层的物理网络设备组成的网络我们称为Underlay网络,而用于虚拟机和云中的这些技术组成的网络称为Overlay网络这是一种基于物理网络的虚拟化网络实现。这一节我们重点讲两个Overlay的网络技术。

GRE

第一个技术是GRE,全称Generic Routing Encapsulation,它是一种IP-over-IP的隧道技术。它将IP包封装在GRE包里,外面加上IP头,在隧道的一端封装数据包,并在通路上进行传输,到另外一端的时候解封装。你可以认为Tunnel是一个虚拟的、点对点的连接。

从这个图中可以看到,在GRE头中,前32位是一定会有的,后面的都是可选的。在前4位标识位里面,有标识后面到底有没有可选项?这里面有个很重要的key字段,是一个32位的字段,里面存放的往往就是用于区分用户的Tunnel ID。32位,够任何云平台喝一壶的了!

下面的格式类型专门用于网络虚拟化的GRE包头格式,称为NVGRE,也给网络ID号24位,也完全够用了。

除此之外,GRE还需要有一个地方来封装和解封装GRE的包,这个地方往往是路由器或者有路由功能的Linux机器。

使用GRE隧道,传输的过程就像下面这张图。这里面有两个网段、两个路由器,中间要通过GRE隧道进行通信。当隧道建立之后,会多出两个Tunnel端口,用于封包、解封包。

  1. 主机A在左边的网络,IP地址为192.168.1.102,它想要访问主机B,主机B在右边的网络,IP地址为192.168.2.115。于是发送一个包,源地址为192.168.1.102,目标地址为192.168.2.115。因为要跨网段访问,于是根据默认的default路由表规则,要发给默认的网关192.168.1.1,也即左边的路由器。
  2. 根据路由表,从左边的路由器,去192.168.2.0/24这个网段,应该走一条GRE的隧道,从隧道一端的网卡Tunnel0进入隧道。
  3. 在Tunnel隧道的端点进行包的封装,在内部的IP头之外加上GRE头。对于NVGRE来讲,是在MAC头之外加上GRE头,然后加上外部的IP地址,也即路由器的外网IP地址。源IP地址为172.17.10.10,目标IP地址为172.16.11.10,然后从E1的物理网卡发送到公共网络里。
  4. 在公共网络里面,沿着路由器一跳一跳地走,全部都按照外部的公网IP地址进行。
  5. 当网络包到达对端路由器的时候,也要到达对端的Tunnel0,然后开始解封装,将外层的IP头取下来,然后根据里面的网络包,根据路由表,从E3口转发出去到达服务器B。

从GRE的原理可以看出,GRE通过隧道的方式,很好地解决了VLAN ID不足的问题。但是,GRE技术本身还是存在一些不足之处。

首先是Tunnel的数量问题。GRE是一种点对点隧道,如果有三个网络,就需要在每两个网络之间建立一个隧道。如果网络数目增多,这样隧道的数目会呈指数性增长。

其次,GRE不支持组播,因此一个网络中的一个虚机发出一个广播帧后,GRE会将其广播到所有与该节点有隧道连接的节点。

另外一个问题是目前还是有很多防火墙和三层网络设备无法解析GRE,因此它们无法对GRE封装包做合适地过滤和负载均衡。

VXLAN

第二种Overlay的技术称为VXLAN。和三层外面再套三层的GRE不同,VXLAN则是从二层外面就套了一个VXLAN的头,这里面包含的VXLAN ID为24位,也够用了。在VXLAN头外面还封装了UDP、IP,以及外层的MAC头。

VXLAN作为扩展性协议,也需要一个地方对VXLAN的包进行封装和解封装,实现这个功能的点称为VTEP(VXLAN Tunnel Endpoint)。

VTEP相当于虚拟机网络的管家。每台物理机上都可以有一个VTEP。每个虚拟机启动的时候,都需要向这个VTEP管家注册,每个VTEP都知道自己上面注册了多少个虚拟机。当虚拟机要跨VTEP进行通信的时候,需要通过VTEP代理进行,由VTEP进行包的封装和解封装。

和GRE端到端的隧道不同,VXLAN不是点对点的,而是支持通过组播的来定位目标机器的,而非一定是这一端发出,另一端接收。

当一个VTEP启动的时候,它们都需要通过IGMP协议。加入一个组播组,就像加入一个邮件列表,或者加入一个微信群一样,所有发到这个邮件列表里面的邮件,或者发送到微信群里面的消息,大家都能收到。而当每个物理机上的虚拟机启动之后,VTEP就知道,有一个新的VM上线了,它归我管。

1.Table 0是所有流量的入口,所有进入br1的流量,分为两种流量,一个是进入物理机的流量,一个是从物理机发出的流量。

2.Table 1用于处理所有出去的网络包,分为两种情况,一种是单播,一种是多播。

3.Table 2是紧接着Table1的,如果既不是单播,也不是多播,就默认丢弃。

4.Table 3用于处理所有进来的网络包,需要将隧道Tunnel ID转换为VLAN ID。

5.对于进来的包,Table 10会进行MAC地址学习。这是一个二层交换机应该做的事情,学习完了之后,再从port 1发出去。

6.Table 20是MAC Address Learning Table。如果不为空,就按照规则处理;如果为空,就说明没有进行过MAC地址学习,只好进行广播了,因而要交给Table 21处理。

7.Table 21用于处理多播的包。

如果匹配不上VLAN ID,就默认丢弃。

  • 要对不同用户的网络进行隔离,解决VLAN数目有限的问题,需要通过Overlay的方式,常用的有GRE和VXLAN。
  • GRE是一种点对点的隧道模式,VXLAN支持组播的隧道模式,它们都要在某个Tunnel Endpoint进行封装和解封装,来实现跨物理机的互通。
  • OpenvSwitch可以作为Tunnel Endpoint,通过设置流表的规则,将虚拟机网络和物理机网络进行隔离、转换。

如果说虚拟机是买公寓,容器则相当于合租,有一定的隔离,但是隔离性没有那么好。云计算解决了基础资源层的弹性伸缩,却没有解决PaaS层应用随基础资源层弹性伸缩而带来的批量、快速部署问题。于是,容器应运而生。

容器就是Container,而Container的另一个意思是集装箱。其实容器的思想就是要变成软件交付的集装箱。集装箱的特点,一是打包,二是标准。

在没有集装箱的时代,假设要将货物从A运到B,中间要经过三个码头、换三次船。每次都要将货物卸下船来,弄的乱七八糟,然后还要再搬上船重新整齐摆好。因此在没有集装箱的时候,每次换船,船员们都要在岸上待几天才能干完活。

有了尺寸全部都一样的集装箱以后,可以把所有的货物都打包在一起,所以每次换船的时候,一个箱子整体搬过去就行了,小时级别就能完成,船员再也不用耗费很长时间了。这是集装箱的“打包”“标准”两大特点在生活中的应用。

那么容器如何对应用打包呢?

学习集装箱,首先要有个封闭的环境,将货物封装起来,让货物之间互不干扰,互相隔离,这样装货卸货才方便。

封闭的环境主要使用了两种技术,一种是看起来是隔离的技术,称为namespace,也即每个 namespace中的应用看到的是不同的 IP地址、用户空间、程号等。另一种是用起来是隔离的技术,称为cgroup,也即明明整台机器有很多的 CPU、内存,而一个应用只能用其中的一部分。

有了这两项技术,就相当于我们焊好了集装箱。接下来的问题就是如何“将这个集装箱标准化”,并在哪艘船上都能运输。这里的标准首先就是镜像

所谓镜像,就是将你焊好集装箱的那一刻,将集装箱的状态保存下来,就像孙悟空说:“定!”,集装箱里的状态就被定在了那一刻,然后将这一刻的状态保存成一系列文件。无论从哪里运行这个镜像,都能完整地还原当时的情况。

接下来我们就具体来看看,这两种网络方面的打包技术。

命名空间(namespace)

我们首先来看网络namespace。

namespace翻译过来就是命名空间。其实很多面向对象的程序设计语言里面,都有命名空间这个东西。大家一起写代码,难免类会起相同的名词,编译就会冲突。而每个功能都有自己的命名空间,在不同的空间里面,类名相同,不会冲突。

在Linux下也是这样的,很多的资源都是全局的。比如进程有全局的进程ID,网络也有全局的路由表。但是,当一台Linux上跑多个进程的时候,如果我们觉得使用不同的路由策略,这些进程可能会冲突,那就需要将这个进程放在一个独立的namespace里面,这样就可以独立配置网络了。

网络的namespace由ip netns命令操作。它可以创建、删除、查询namespace。

我们再来看将你们宿舍放进一台物理机的那个图。你们宿舍长的电脑是一台路由器,你现在应该知道怎么实现这个路由器吧?可以创建一个Router虚拟机来做这件事情,但是还有一个更加简单的办法,就是我在图里画的这条虚线,这个就是通过namespace实现的。

机制网络(cgroup)

我们再来看打包的另一个机制网络cgroup。

cgroup全称control groups,是Linux内核提供的一种可以限制、隔离进程使用的资源机制。

cgroup能控制哪些资源呢?它有很多子系统:

  • CPU子系统使用调度程序为进程控制CPU的访问;
  • cpuset,如果是多核心的CPU,这个子系统会为进程分配单独的CPU和内存;
  • memory子系统,设置进程的内存限制以及产生内存资源报告;
  • blkio子系统,设置限制每个块设备的输入输出控制;
  • net_cls,这个子系统使用等级识别符(classid)标记网络数据包,可允许Linux 流量控制程序(tc)识别从具体cgroup中生成的数据包。

我们这里最关心的是net_cls,它可以和前面讲过的TC关联起来。

cgroup提供了一个虚拟文件系统,作为进行分组管理和各子系统设置的用户接口。要使用cgroup,必须挂载cgroup文件系统,一般情况下都是挂载到/sys/fs/cgroup目录下。

容器网络中如何融入物理网络?

了解了容器背后的技术,接下来我们来看,容器网络究竟是如何融入物理网络的?

如果你使用docker run运行一个容器,你应该能看到这样一个拓扑结构。

是不是和虚拟机很像?容器里面有张网卡,容器外有张网卡,容器外的网卡连到docker0网桥,通过这个网桥,容器直接实现相互访问。

如果你用brctl查看docker0网桥,你会发现它上面连着一些网卡。

一台机器内部容器的互相访问没有问题了,那如何访问外网呢?

你先想想看有没有思路?对,就是虚拟机里面的桥接模式和NAT模式。Docker默认使用NAT模式。NAT模式分为SNAT和DNAT,如果是容器内部访问外部,就需要通过SNAT。

所有从容器内部发出来的包,都要做地址伪装,将源IP地址,转换为物理网卡的IP地址。如果有多个容器,所有的容器共享一个外网的IP地址,但是在conntrack表中,记录下这个出去的连接。

当服务器返回结果的时候,到达物理机,会根据conntrack表中的规则,取出原来的私网IP,通过DNAT将地址转换为私网IP地址,通过网桥docker0实现对内的访问。

如果在容器内部属于一个服务,例如部署一个网站,提供给外部进行访问,需要通过Docker的端口映射技术,将容器内部的端口映射到物理机上来。

例如容器内部监听80端口,可以通Docker run命令中的参数-p 10080:80,将物理机上的10080端口和容器的80端口映射起来, 当外部的客户端访问这个网站的时候,通过访问物理机的10080端口,就能访问到容器内的80端口了。

好了,这一节就到这里了,我们来总结一下。

  • 容器是一种比虚拟机更加轻量级的隔离方式,主要通过namespace和cgroup技术进行资源的隔离,namespace用于负责看起来隔离,cgroup用于负责用起来隔离。
  • 容器网络连接到物理网络的方式和虚拟机很像,通过桥接的方式实现一台物理机上的容器进行相互访问,如果要访问外网,最简单的方式还是通过NAT。

每一台物理机上面安装好了Docker以后,都会默认分配一个172.17.0.0/16的网段。一台机器上新创建的第一个容器,一般都会给172.17.0.2这个地址,当然一台机器这样玩玩倒也没啥问题。但是容器里面是要部署应用的,就像上一节讲过的一样,它既然是集装箱,里面就需要装载货物。

如果这个应用是比较传统的单体应用,自己就一个进程,所有的代码逻辑都在这个进程里面,上面的模式没有任何问题,只要通过NAT就能访问进来。

但是因为无法解决快速迭代和高并发的问题,单体应用越来越跟不上时代发展的需要了。

你可以回想一下,无论是各种网络直播平台,还是共享单车,是不是都是很短时间内就要积累大量用户,否则就会错过风口。所以应用需要在很短的时间内快速迭代,不断调整,满足用户体验;还要在很短的时间内,具有支撑高并发请求的能力。

单体应用作为个人英雄主义的时代已经过去了。如果所有的代码都在一个工程里面,开发的时候必然存在大量冲突,上线的时候,需要开大会进行协调,一个月上线一次就很不错了。而且所有的流量都让一个进程扛,怎么也扛不住啊!

没办法,一个字:拆!拆开了,每个子模块独自变化,减少相互影响。拆开了,原来一个进程扛流量,现在多个进程一起扛。所以,微服务就是从个人英雄主义,变成集团军作战。

容器作为集装箱,可以保证应用在不同的环境中快速迁移,提高迭代的效率。但是如果要形成容器集团军,还需要一个集团军作战的调度平台,这就是Kubernetes。它可以灵活地将一个容器调度到任何一台机器上,并且当某个应用扛不住的时候,只要在Kubernetes上修改容器的副本数,一个应用马上就能变八个,而且都能提供服务。

然而集团军作战有个重要的问题,就是通信。这里面包含两个问题,第一个是集团军的A部队如何实时地知道B部队的位置变化,第二个是两个部队之间如何相互通信。

第一个问题位置变化,往往是通过一个称为注册中心的地方统一管理的,这个是应用自己做的。当一个应用启动的时候,将自己所在环境的IP地址和端口,注册到注册中心指挥部,这样其他的应用请求它的时候,到指挥部问一下它在哪里就好了。当某个应用发生了变化,例如一台机器挂了,容器要迁移到另一台机器,这个时候IP改变了,应用会重新注册,则其他的应用请求它的时候,还是能够从指挥部得到最新的位置。

这样存在的问题是,应用是在容器里面的,它怎么知道物理机上的IP地址和端口呢?这明明是运维人员配置的,除非应用配合,读取容器平台的接口获得这个IP和端口。一方面,大部分分布式框架都是容器诞生之前就有了,它们不会适配这种场景;另一方面,让容器内的应用意识到容器外的环境,本来就是非常不好的设计。

说好的集装箱,说好的随意迁移呢?难道要让集装箱内的货物意识到自己传的信息?而且本来Tomcat都是监听8080端口的,结果到了物理机上,就不能大家都用这个端口了,否则端口就冲突了,因而就需要随机分配端口,于是在注册中心就出现了各种各样奇怪的端口。无论是注册中心,还是调用方都会觉得很奇怪,而且不是默认的端口,很多情况下也容易出错。

Kubernetes作为集团军作战管理平台,提出指导意见,说网络模型要变平,但是没说怎么实现。于是业界就涌现了大量的方案,Flannel就是其中之一。

对于IP冲突的问题,如果每一个物理机都是网段172.17.0.0/16,肯定会冲突啊,但是这个网段实在太大了,一台物理机上根本启动不了这么多的容器,所以能不能每台物理机在这个大网段里面,抠出一个小的网段,每个物理机网段都不同,自己看好自己的一亩三分地,谁也不和谁冲突。

例如物理机A是网段172.17.8.0/24,物理机B是网段172.17.9.0/24,这样两台机器上启动的容器IP肯定不一样,而且就看IP地址,我们就一下子识别出,这个容器是本机的,还是远程的,如果是远程的,也能从网段一下子就识别出它归哪台物理机管,太方便了。

接下来的问题,就是物理机A上的容器如何访问到物理机B上的容器呢?

你是不是想到了熟悉的场景?虚拟机也需要跨物理机互通,往往通过Overlay的方式,容器是不是也可以这样做呢?

在物理机A上的容器A里面,能看到的容器的IP地址是172.17.8.2/24,里面设置了默认的路由规则default via 172.17.8.1 dev eth0。

如果容器A要访问172.17.9.2,就会发往这个默认的网关172.17.8.1。172.17.8.1就是物理机上面docker0网桥的IP地址,这台物理机上的所有容器都是连接到这个网桥的。

在物理机上面,查看路由策略,会有这样一条172.17.0.0/24 via 172.17.0.0 dev flannel.1,也就是说发往172.17.9.2的网络包会被转发到flannel.1这个网卡。

这个网卡是怎么出来的呢?在每台物理机上,都会跑一个flanneld进程,这个进程打开一个/dev/net/tun字符设备的时候,就出现了这个网卡。

你有没有想起qemu-kvm,打开这个字符设备的时候,物理机上也会出现一个网卡,所有发到这个网卡上的网络包会被qemu-kvm接收进来,变成二进制串。只不过接下来qemu-kvm会模拟一个虚拟机里面的网卡,将二进制的串变成网络包,发给虚拟机里面的网卡。但是flanneld不用这样做,所有发到flannel.1这个网卡的包都会被flanneld进程读进去,接下来flanneld要对网络包进行处理。

物理机A上的flanneld会将网络包封装在UDP包里面,然后外层加上物理机A和物理机B的IP地址,发送给物理机B上的flanneld。

为什么是UDP呢?因为不想在flanneld之间建立两两连接,而UDP没有连接的概念,任何一台机器都能发给另一台。

物理机B上的flanneld收到包之后,解开UDP的包,将里面的网络包拿出来,从物理机B的flannel.1网卡发出去。

在物理机B上,有路由规则172.17.9.0/24 dev docker0 proto kernel scope link src 172.17.9.1。

将包发给docker0,docker0将包转给容器B。通信成功。

上面的过程连通性没有问题,但是由于全部在用户态,所以性能差了一些。

跨物理机的连通性问题,在虚拟机那里有成熟的方案,就是VXLAN,那能不能Flannel也用VXLAN呢

当然可以了。如果使用VXLAN,就不需要打开一个TUN设备了,而是要建立一个VXLAN的VTEP。如何建立呢?可以通过netlink通知内核建立一个VTEP的网卡flannel.1。在我们讲OpenvSwitch的时候提过,netlink是一种用户态和内核态通信的机制。

当网络包从物理机A上的容器A发送给物理机B上的容器B,在容器A里面通过默认路由到达物理机A上的docker0网卡,然后根据路由规则,在物理机A上,将包转发给flannel.1。这个时候flannel.1就是一个VXLAN的VTEP了,它将网络包进行封装。

内部的MAC地址这样写:源为物理机A的flannel.1的MAC地址,目标为物理机B的flannel.1的MAC地址,在外面加上VXLAN的头。

外层的IP地址这样写:源为物理机A的IP地址,目标为物理机B的IP地址,外面加上物理机的MAC地址。

这样就能通过VXLAN将包转发到另一台机器,从物理机B的flannel.1上解包,变成内部的网络包,通过物理机B上的路由转发到docker0,然后转发到容器B里面。通信成功。

  • 基于NAT的容器网络模型在微服务架构下有两个问题,一个是IP重叠,一个是端口冲突,需要通过Overlay网络的机制保持跨节点的连通性。
  • Flannel是跨节点容器网络方案之一,它提供的Overlay方案主要有两种方式,一种是UDP在用户态封装,一种是VXLAN在内核态封装,而VXLAN的性能更好一些。

上一节我们讲了Flannel如何解决容器跨主机互通的问题,这个解决方式其实和虚拟机的网络互通模式是差不多的,都是通过隧道。但是Flannel有一个非常好的模式,就是给不同的物理机设置不同网段,这一点和虚拟机的Overlay的模式完全不一样。

在虚拟机的场景下,整个网段在所有的物理机之间都是可以“飘来飘去”的。网段不同,就给了我们做路由策略的可能。

Calico网络模型的设计思路

我们看图中的两台物理机。它们的物理网卡是同一个二层网络里面的。由于两台物理机的容器网段不同,我们完全可以将两台物理机配置成为路由器,并按照容器的网段配置路由表。

例如,在物理机A中,我们可以这样配置:要想访问网段172.17.9.0/24,下一跳是192.168.100.101,也即到物理机B上去。

这样在容器A中访问容器B,当包到达物理机A的时候,就能够匹配到这条路由规则,并将包发给下一跳的路由器,也即发给物理机B。在物理机B上也有路由规则,要访问172.17.9.0/24,从docker0的网卡进去即可。

当容器B返回结果的时候,在物理机B上,可以做类似的配置:要想访问网段172.17.8.0/24,下一跳是192.168.100.100,也即到物理机A上去。

当包到达物理机B的时候,能够匹配到这条路由规则,将包发给下一跳的路由器,也即发给物理机A。在物理机A上也有路由规则,要访问172.17.8.0/24,从docker0的网卡进去即可。

这就是Calico网络的大概思路即不走Overlay网络,不引入另外的网络性能损耗,而是将转发全部用三层网络的路由转发来实现,只不过具体的实现和上面的过程稍有区别。

首先,如果全部走三层的路由规则,没必要每台机器都用一个docker0,从而浪费了一个IP地址,而是可以直接用路由转发到veth pair在物理机这一端的网卡。同样,在容器内,路由规则也可以这样设定:把容器外面的veth pair网卡算作默认网关,下一跳就是外面的物理机。

Calico网络的转发细节

我们来看其中的一些细节。

容器A1的IP地址为172.17.8.2/32,这里注意,不是/24,而是/32,将容器A1作为一个单点的局域网了。

容器A1里面的默认路由,Calico配置得比较有技巧。

1
2
default via 169.254.1.1 dev eth0 
169.254.1.1 dev eth0 scope link

这个IP地址169.254.1.1是默认的网关,但是整个拓扑图中没有一张网卡是这个地址。那如何到达这个地址呢?

前面我们讲网关的原理的时候说过,当一台机器要访问网关的时候,首先会通过ARP获得网关的MAC地址,然后将目标MAC变为网关的MAC,而网关的IP地址不会在任何网络包头里面出现,也就是说,没有人在乎这个地址具体是什么,只要能找到对应的MAC,响应ARP就可以了。

ARP本地有缓存,通过ip neigh命令可以查看。

1
169.254.1.1 dev eth0 lladdr ee:ee:ee:ee:ee:ee STALE

这个MAC地址是Calico硬塞进去的,但是没有关系,它能响应ARP,于是发出的包的目标MAC就是这个MAC地址。

在物理机A上查看所有网卡的MAC地址的时候,我们会发现veth1就是这个MAC地址。所以容器A1里发出的网络包,第一跳就是这个veth1这个网卡,也就到达了物理机A这个路由器。

在物理机A上有三条路由规则,分别是去两个本机的容器的路由,以及去172.17.9.0/24,下一跳为物理机B。

1
2
3
172.17.8.2 dev veth1 scope link 
172.17.8.3 dev veth2 scope link
172.17.9.0/24 via 192.168.100.101 dev eth0 proto bird onlink

同理,物理机B上也有三条路由规则,分别是去两个本机的容器的路由,以及去172.17.8.0/24,下一跳为物理机A。

1
2
3
172.17.9.2 dev veth1 scope link 
172.17.9.3 dev veth2 scope link
172.17.8.0/24 via 192.168.100.100 dev eth0 proto bird onlink

如果你觉得这些规则过于复杂,我将刚才的拓扑图转换为这个更加容易理解的图。

在这里,物理机化身为路由器,通过路由器上的路由规则,将包转发到目的地。在这个过程中,没有隧道封装解封装,仅仅是单纯的路由转发,性能会好很多。但是,这种模式也有很多问题。

Calico的架构

路由配置组件Felix

如果只有两台机器,每台机器只有两个容器,而且保持不变。我手动配置一下,倒也没啥问题。但是如果容器不断地创建、删除,节点不断地加入、退出,情况就会变得非常复杂。

就像图中,有三台物理机,两两之间都需要配置路由,每台物理机上对外的路由就有两条。如果有六台物理机,则每台物理机上对外的路由就有五条。新加入一个节点,需要通知每一台物理机添加一条路由。

这还是在物理机之间,一台物理机上,每创建一个容器,也需要多配置一条指向这个容器的路由。如此复杂,肯定不能手动配置,需要每台物理机上有一个agent,当创建和删除容器的时候,自动做这件事情。这个agent在Calico中称为Felix。

路由广播组件BGP Speaker

当Felix配置了路由之后,接下来的问题就是,如何将路由信息,也即将“如何到达我这个节点,访问我这个节点上的容器”这些信息,广播出去。

能想起来吗?这其实就是路由协议啊!路由协议就是将“我能到哪里,如何能到我”的信息广播给全网传出去,从而客户端可以一跳一跳地访问目标地址的。路由协议有很多种,Calico使用的是BGP协议。

在Calico中,每个Node上运行一个软件BIRD,作为BGP的客户端,或者叫作BGP Speaker,将“如何到达我这个Node,访问我这个Node上的容器”的路由信息广播出去。所有Node上的BGP Speaker 都互相建立连接,就形成了全互连的情况,这样每当路由有所变化的时候,所有节点就都能够收到了。

安全策略组件

Calico中还实现了灵活配置网络策略Network Policy,可以灵活配置两个容器通或者不通。这个怎么实现呢?

虚拟机中的安全组,是用iptables实现的。Calico中也是用iptables实现的。这个图里的内容是iptables在内核处理网络包的过程中可以嵌入的处理点。Calico也是在这些点上设置相应的规则。

当网络包进入物理机上的时候,进入PREOUTING规则,这里面有一个规则是cali-fip-dnat,这是实现浮动IP(Floating IP)的场景,主要将外网的IP地址dnat为容器内的IP地址。在虚拟机场景下,路由器的网络namespace里面有一个外网网卡上,也设置过这样一个DNAT规则。

接下来可以根据路由判断,是到本地的,还是要转发出去的。

如果是本地的,走INPUT规则,里面有个规则是cali-wl-to-host,wl的意思是workload,也即容器,也即这是用来判断从容器发到物理机的网络包是否符合规则的。这里面内嵌一个规则cali-from-wl-dispatch,也是匹配从容器来的包。如果有两个容器,则会有两个容器网卡,这里面内嵌有详细的规则“cali-fw-cali网卡1”和“cali-fw-cali网卡2”,fw就是from workload,也就是匹配从容器1来的网络包和从容器2来的网络包。

如果是转发出去的,走FORWARD规则,里面有个规则cali-FORWARD。这里面分两种情况,一种是从容器里面发出来,转发到外面的;另一种是从外面发进来,转发到容器里面的。

第一种情况匹配的规则仍然是cali-from-wl-dispatch,也即from workload。第二种情况匹配的规则是cali-to-wl-dispatch,也即to workload。如果有两个容器,则会有两个容器网卡,在这里面内嵌有详细的规则“cali-tw-cali网卡1”和“cali-tw-cali网卡2”,tw就是to workload,也就是匹配发往容器1的网络包和发送到容器2的网络包。

接下来是匹配OUTPUT规则,里面有cali-OUTPUT。接下来是POSTROUTING规则,里面有一个规则是cali-fip-snat,也即发出去的时候,将容器网络IP转换为浮动IP地址。在虚拟机场景下,路由器的网络namespace里面有一个外网网卡上,也设置过这样一个SNAT规则。

全连接复杂性与规模问题

这里面还存在问题,就是BGP全连接的复杂性问题。

你看刚才的例子里只有六个节点,BGP的互连已经如此复杂,如果节点数据再多,这种全互连的模式肯定不行,到时候都成蜘蛛网了。于是多出了一个组件BGP Route Reflector,它也是用BIRD实现的。有了它,BGP Speaker就不用全互连了,而是都直连它,它负责将全网的路由信息广播出去。

可是问题来了,规模大了,大家都连它,它受得了吗?这个BGP Router Reflector会不会成为瓶颈呢?

所以,肯定不能让一个BGP Router Reflector管理所有的路由分发,而是应该有多个BGP Router Reflector,每个BGP Router Reflector管一部分。

多大算一部分呢?咱们讲述数据中心的时候,说服务器都是放在机架上的,每个机架上最顶端有个TOR交换机。那将机架上的机器连在一起,这样一个机架是不是可以作为一个单元,让一个BGP Router Reflector来管理呢?如果要跨机架,如何进行通信呢?这就需要BGP Router Reflector也直接进行路由交换。它们之间的交换和一个机架之间的交换有什么关系吗?

有没有觉得在这个场景下,一个机架就像一个数据中心,可以把它设置为一个AS,而BGP Router Reflector有点儿像数据中心的边界路由器。在一个AS内部,也即服务器和BGP Router Reflector之间使用的是数据中心内部的路由协议iBGP,BGP Router Reflector之间使用的是数据中心之间的路由协议eBGP。

这个图中,一个机架上有多台机器,每台机器上面启动多个容器,每台机器上都有可以到达这些容器的路由。每台机器上都启动一个BGP Speaker,然后将这些路由规则上报到这个Rack上接入交换机的BGP Route Reflector,将这些路由通过iBGP协议告知到接入交换机的三层路由功能。

在接入交换机之间也建立BGP连接,相互告知路由,因而一个Rack里面的路由可以告知另一个Rack。有多个核心或者汇聚交换机将接入交换机连接起来,如果核心和汇聚起二层互通的作用,则接入和接入之间之间交换路由即可。如果核心和汇聚交换机起三层路由的作用,则路由需要通过核心或者汇聚交换机进行告知。

跨网段访问问题

上面的Calico模式还有一个问题,就是跨网段问题,这里的跨网段是指物理机跨网段。

前面我们说的那些逻辑成立的条件,是我们假设物理机可以作为路由器进行使用。例如物理机A要告诉物理机B,你要访问172.17.8.0/24,下一跳是我192.168.100.100;同理,物理机B要告诉物理机A,你要访问172.17.9.0/24,下一跳是我192.168.100.101。

之所以能够这样,是因为物理机A和物理机B是同一个网段的,是连接在同一个交换机上的。

例如,物理机A的网段是192.168.100.100/24,物理机B的网段是192.168.200.101/24,这样两台机器就不能通过二层交换机连接起来了,需要在中间放一台路由器,做一次路由转发,才能跨网段访问。

本来物理机A要告诉物理机B,你要访问172.17.8.0/24,下一跳是我192.168.100.100的,但是中间多了一台路由器,下一跳不是我了,而是中间的这台路由器了,这台路由器的再下一跳,才是我。这样之前的逻辑就不成立了。

我们看刚才那张图的下半部分。物理机B上的容器要访问物理机A上的容器,第一跳就是物理机B,IP为192.168.200.101,第二跳是中间的物理路由器右面的网口,IP为192.168.200.1,第三跳才是物理机A,IP为192.168.100.100。

这是咱们通过拓扑图看到的,关键问题是,在系统中物理机A如何告诉物理机B,怎么让它才能到我这里?物理机A根本不可能知道从物理机B出来之后的下一跳是谁,况且现在只是中间隔着一个路由器这种简单的情况,如果隔着多个路由器呢?谁能把这一串的路径告诉物理机B呢?

我们能想到的第一种方式是,让中间所有的路由器都来适配Calico。本来它们互相告知路由,只互相告知物理机的,现在还要告知容器的网段。这在大部分情况下,是不可能的。

第二种方式,还是在物理机A和物理机B之间打一个隧道,这个隧道有两个端点,在端点上进行封装,将容器的IP作为乘客协议放在隧道里面,而物理主机的IP放在外面作为承载协议。这样不管外层的IP通过传统的物理网络,走多少跳到达目标物理机,从隧道两端看起来,物理机A的下一跳就是物理机B,这样前面的逻辑才能成立。

这就是Calico的IPIP模式。使用了IPIP模式之后,在物理机A上,我们能看到这样的路由表:

这和原来模式的区别在于,下一跳不再是同一个网段的物理机B了,IP为192.168.200.101,并且不是从eth0跳,而是建立一个隧道的端点tun0,从这里才是下一跳。

如果我们在容器A1里面的172.17.8.2,去ping容器B1里面的172.17.9.2,首先会到物理机A。在物理机A上根据上面的规则,会转发给tun0,并在这里对包做封装:

  • 内层源IP为172.17.8.2;
  • 内层目标IP为172.17.9.2;
  • 外层源IP为192.168.100.100;
  • 外层目标IP为192.168.200.101。

将这个包从eth0发出去,在物理网络上会使用外层的IP进行路由,最终到达物理机B。在物理机B上,tun0会解封装,将内层的源IP和目标IP拿出来,转发给相应的容器。

小结

好了,这一节就到这里,我们来总结一下。

  • Calico推荐使用物理机作为路由器的模式,这种模式没有虚拟化开销,性能比较高。
  • Calico的主要组件包括路由、iptables的配置组件Felix、路由广播组件BGP Speaker,以及大规模场景下的BGP Route Reflector。
  • 为解决跨网段的问题,Calico还有一种IPIP模式,也即通过打隧道的方式,从隧道端点来看,将本来不是邻居的两台机器,变成相邻的机器。

首先你要会Socket编程,至少先要把咱们这门网络协议课学一下,然后再看N本砖头厚的Socket程序设计的书,学会咱们学过的几种Socket程序设计的模型。这就使得本来大学毕业就能干的一项工作,变成了一件五年工作经验都不一定干好的工作,而且,搞定了Socket程序设计,才是万里长征的第一步。后面还有很多问题呢!

如何解决这五个问题?

问题一:如何规定远程调用的语法?

客户端如何告诉服务端,我是一个加法,而另一个是乘法。我是用字符串“add”传给你,还是传给你一个整数,比如1表示加法,2表示乘法?服务端该如何告诉客户端,我的这个加法,目前只能加整数,不能加小数,不能加字符串;而另一个加法“add1”,它能实现小数和整数的混合加法。那返回值是什么?正确的时候返回什么,错误的时候又返回什么?

问题二:如果传递参数?

我是先传两个整数,后传一个操作符“add”,还是先传操作符,再传两个整数?是不是像咱们数据结构里一样,如果都是UDP,想要实现一个逆波兰表达式,放在一个报文里面还好,如果是TCP,是一个流,在这个流里面,如何将两次调用进行分界?什么时候是头,什么时候是尾?别这次的参数和上次的参数混了起来,TCP一端发送出去的数据,另外一端不一定能一下子全部读取出来。所以,怎么才算读完呢?

问题三:如何表示数据?

在这个简单的例子中,传递的就是一个固定长度的int值,这种情况还好,如果是变长的类型,是一个结构体,甚至是一个类,应该怎么办呢?如果是int,不同的平台上长度也不同,该怎么办呢?

在网络上传输超过一个Byte的类型,还有大端Big Endian和小端Little Endian的问题。

假设我们要在32位四个Byte的一个空间存放整数1,很显然只要一个Byte放1,其他三个Byte放0就可以了。那问题是,最后一个Byte放1呢,还是第一个Byte放1呢?或者说1作为最低位,应该是放在32位的最后一个位置呢,还是放在第一个位置呢?

最低位放在最后一个位置,叫作Little Endian,最低位放在第一个位置,叫作Big Endian。TCP/IP协议栈是按照Big Endian来设计的,而X86机器多按照Little Endian来设计的,因而发出去的时候需要做一个转换。

问题四:如何知道一个服务端都实现了哪些远程调用?从哪个端口可以访问这个远程调用?

假设服务端实现了多个远程调用,每个可能实现在不同的进程中,监听的端口也不一样,而且由于服务端都是自己实现的,不可能使用一个大家都公认的端口,而且有可能多个进程部署在一台机器上,大家需要抢占端口,为了防止冲突,往往使用随机端口,那客户端如何找到这些监听的端口呢?

问题五:发生了错误、重传、丢包、性能等问题怎么办?

本地调用没有这个问题,但是一旦到网络上,这些问题都需要处理,因为网络是不可靠的,虽然在同一个连接中,我们还可通过TCP协议保证丢包、重传的问题,但是如果服务器崩溃了又重启,当前连接断开了,TCP就保证不了了,需要应用自己进行重新调用,重新传输会不会同样的操作做两遍,远程调用性能会不会受影响呢?

本地调用函数里有很多问题,比如词法分析、语法分析、语义分析等等,这些编译器本来都能帮你做了。但是在远程调用中,这些问题你都需要重新操心。

很多公司的解决方法是,弄一个核心通信组,里面都是Socket编程的大牛,实现一个统一的库,让其他业务组的人来调用,业务的人不需要知道中间传输的细节。通信双方的语法、语义、格式、端口、错误处理等,都需要调用方和被调用方开会商量,双方达成一致。一旦有一方改变,要及时通知对方,否则通信就会有问题。

可是不是每一个公司都有这种大牛团队,往往只有大公司才配得起,那有没有已经实现好的框架可以使用呢?

当然有。一个大牛Bruce Jay Nelson写了一篇论文Implementing Remote Procedure Calls,定义了RPC的调用标准。后面所有RPC框架,都是按照这个标准模式来的。

当客户端的应用想发起一个远程调用时,它实际是通过本地调用本地调用方的Stub。它负责将调用的接口、方法和参数,通过约定的协议规范进行编码,并通过本地的RPCRuntime进行传输,将调用网络包发送到服务器。

服务器端的RPCRuntime收到请求后,交给提供方Stub进行解码,然后调用服务端的方法,服务端执行方法,返回结果,提供方Stub将返回结果编码后,发送给客户端,客户端的RPCRuntime收到结果,发给调用方Stub解码得到结果,返回给客户端。

这里面分了三个层次,对于用户层和服务端,都像是本地调用一样,专注于业务逻辑的处理就可以了。对于Stub层,处理双方约定好的语法、语义、封装、解封装。对于RPCRuntime,主要处理高性能的传输,以及网络的错误和异常。

最早的RPC的一种实现方式称为Sun RPC或ONC RPC。Sun公司是第一个提供商业化RPC库和 RPC编译器的公司。这个RPC框架是在NFS协议中使用的。

NFS(Network File System)就是网络文件系统。要使NFS成功运行,要启动两个服务端,一个是mountd,用来挂载文件路径;一个是nfsd,用来读写文件。NFS可以在本地mount一个远程的目录到本地的一个目录,从而本地的用户在这个目录里面写入、读出任何文件的时候,其实操作的是远程另一台机器上的文件。

操作远程和远程调用的思路是一样的,就像操作本地一样。所以NFS协议就是基于RPC实现的。当然无论是什么RPC,底层都是Socket编程。

在RPC的调用过程中,所有的数据类型都要封装成类似的格式。而且RPC的调用和结果返回,也有严格的格式。

  • XID唯一标识一对请求和回复。请求为0,回复为1。
  • RPC有版本号,两端要匹配RPC协议的版本号。如果不匹配,就会返回Deny,原因就是RPC_MISMATCH。
  • 程序有编号。如果服务端找不到这个程序,就会返回PROG_UNAVAIL。
  • 程序有版本号。如果程序的版本号不匹配,就会返回PROG_MISMATCH。
  • 一个程序可以有多个方法,方法也有编号,如果找不到方法,就会返回PROC_UNAVAIL。
  • 调用需要认证鉴权,如果不通过,则Deny。
  • 最后是参数列表,如果参数无法解析,则返回GABAGE_ARGS。

最下层的是XDR文件,用于编码和解码参数。这个文件是客户端和服务端共享的,因为只有双方一致才能成功通信。

在客户端,会调用clnt_create创建一个连接,然后调用add_1,这是一个Stub函数,感觉是在调用本地一样。其实是这个函数发起了一个RPC调用,通过调用clnt_call来调用ONC RPC的类库,来真正发送请求。调用的过程非常复杂,一会儿我详细说这个。

当然服务端也有一个Stub程序,监听客户端的请求,当调用到达的时候,判断如果是add,则调用真正的服务端逻辑,也即将两个数加起来。

服务端将结果返回服务端的Stub,这个Stub程序发送结果给客户端,客户端的Stub程序正在等待结果,当结果到达客户端Stub,就将结果返回给客户端的应用程序,从而完成整个调用过程。

有了这个RPC的框架,前面五个问题中的前三个“如何规定远程调用的语法?”“如何传递参数?”以及“如何表示数据?”基本解决了,这三个问题我们统称为协议约定问题

传输问题

但是错误、重传、丢包、性能等问题还没有解决,这些问题我们统称为传输问题。这个就不用Stub操心了,而是由ONC RPC的类库来实现。这是大牛们实现的,我们只要调用就可以了。

从这个图可以看出,这个状态转换图还是很复杂的。

首先,进入起始状态,查看RPC的传输层队列中有没有空闲的位置,可以处理新的RPC任务。如果没有,说明太忙了,或直接结束或重试。如果申请成功,就可以分配内存,获取服务的端口号,然后连接服务器。

连接的过程要有一段时间,因而要等待连接的结果,会有连接失败,或直接结束或重试。如果连接成功,则开始发送RPC请求,然后等待获取RPC结果,这个过程也需要一定的时间;如果发送出错,可以重新发送;如果连接断了,可以重新连接;如果超时,可以重新传输;如果获取到结果,就可以解码,正常结束。

这里处理了连接失败、重试、发送失败、超时、重试等场景。不是大牛真写不出来,因而实现一个RPC的框架,其实很有难度。

服务发现问题

传输问题解决了,我们还遗留一个问题,就是问题四“如何找到RPC服务端的那个随机端口”。这个问题我们称为服务发现问题。在ONC RPC中,服务发现是通过portmapper实现的。

portmapper会启动在一个众所周知的端口上,RPC程序由于是用户自己写的,会监听在一个随机端口上,但是RPC程序启动的时候,会向portmapper注册。客户端要访问RPC服务端这个程序的时候,首先查询portmapper,获取RPC服务端程序的随机端口,然后向这个随机端口建立连接,开始RPC调用。从图中可以看出,mount命令的RPC调用,就是这样实现的。

小结

好了,这一节就到这里,我们来总结一下。

  • 远程调用看起来用Socket编程就可以了,其实是很复杂的,要解决协议约定问题、传输问题和服务发现问题。
  • 大牛Bruce Jay Nelson的论文、早期ONC RPC框架,以及NFS的实现,给出了解决这三大问题的示范性实现,也即协议约定要公用协议描述文件,并通过这个文件生成Stub程序;RPC的传输一般需要一个状态机,需要另外一个进程专门做服务发现。

ONC RPC存在哪些问题?

ONC RPC将客户端要发送的参数,以及服务端要发送的回复,都压缩为一个二进制串,这样固然能够解决双方的协议约定问题,但是存在一定的不方便。

首先,需要双方的压缩格式完全一致,一点都不能差。一旦有少许的差错,多一位,少一位或者错一位,都可能造成无法解压缩。当然,我们可以用传输层的可靠性以及加入校验值等方式,来减少传输过程中的差错。

其次,协议修改不灵活。如果不是传输过程中造成的差错,而是客户端因为业务逻辑的改变,添加或者删除了字段,或者服务端添加或者删除了字段,而双方没有及时通知,或者线上系统没有及时升级,就会造成解压缩不成功。

因而,当业务发生改变,需要多传输一些参数或者少传输一些参数的时候,都需要及时通知对方,并且根据约定好的协议文件重新生成双方的Stub程序。自然,这样灵活性比较差。

如果仅仅是沟通的问题也还好解决,其实更难弄的还有版本的问题。比如在服务端提供一个服务,参数的格式是版本一的,已经有50个客户端在线上调用了。现在有一个客户端有个需求,要加一个字段,怎么办呢?这可是一个大工程,所有的客户端都要适配这个,需要重新写程序,加上这个字段,但是传输值是0,不需要这个字段的客户端很“冤”,本来没我啥事儿,为啥让我也忙活?

最后,ONC RPC的设计明显是面向函数的,而非面向对象。而当前面向对象的业务逻辑设计与实现方式已经成为主流。

这一切的根源就在于压缩。这就像平时我们爱用缩略语。如果是篮球爱好者,你直接说NBA,他马上就知道什么意思,但是如果你给一个大妈说NBA,她可能就不知所云。

所以,这种RPC框架只能用于客户端和服务端全由一拨人开发的场景,或者至少客户端和服务端的开发人员要密切沟通,相互合作,有大量的共同语言,才能按照既定的协议顺畅地进行工作。

XML与SOAP

但是,一般情况下,我们做一个服务,都是要提供给陌生人用的,你和客户不会经常沟通,也没有什么共同语言。就像你给别人介绍NBA,你要说美国职业篮球赛,这样不管他是干啥的,都能听得懂。

放到我们的场景中,对应的就是用文本类的方式进行传输。无论哪个客户端获得这个文本,都能够知道它的意义。

传输协议问题

我们先解决第一个,传输协议的问题。

基于XML的最著名的通信协议就是SOAP了,全称简单对象访问协议(Simple Object Access Protocol)。它使用XML编写简单的请求和回复消息,并用HTTP协议进行传输。

SOAP将请求和回复放在一个信封里面,就像传递一个邮件一样。信封里面的信分抬头正文

协议约定问题

接下来我们解决第二个问题,就是双方的协议约定是什么样的?

因为服务开发出来是给陌生人用的,就像上面下单的那个XML文件,对于客户端来说,它如何知道应该拼装成上面的格式呢?这就需要对于服务进行描述,因为调用的人不认识你,所以没办法找到你,问你的服务应该如何调用。

当然你可以写文档,然后放在官方网站上,但是你的文档不一定更新得那么及时,而且你也写的文档也不一定那么严谨,所以常常会有调试不成功的情况。因而,我们需要一种相对比较严谨的Web服务描述语言WSDL(Web Service Description Languages)。它也是一个XML文件。

WSDL还是有些复杂的,不过好在有工具可以生成。

对于某个服务,哪怕是一个陌生人,都可以通过在服务地址后面加上“?wsdl”来获取到这个文件,但是这个文件还是比较复杂,比较难以看懂。不过好在也有工具可以根据WSDL生成客户端Stub,让客户端通过Stub进行远程调用,就跟调用本地的方法一样。

服务发现问题

最后解决第三个问题,服务发现问题。

这里有一个UDDI(Universal Description, Discovery, and Integration),也即统一描述、发现和集成协议。它其实是一个注册中心,服务提供方可以将上面的WSDL描述文件,发布到这个注册中心,注册完毕后,服务使用方可以查找到服务的描述,封装为本地的客户端进行调用。

小结

好了,这一节就到这里了,我们来总结一下。

  • 原来的二进制RPC有很多缺点,格式要求严格,修改过于复杂,不面向对象,于是产生了基于文本的调用方式——基于XML的SOAP。
  • SOAP有三大要素:协议约定用WSDL、传输协议用HTTP、服务发现用UDDL。

WSDL还是有些复杂的,不过好在有工具可以生成。

对于某个服务,哪怕是一个陌生人,都可以通过在服务地址后面加上“?wsdl”来获取到这个文件,但是这个文件还是比较复杂,比较难以看懂。不过好在也有工具可以根据WSDL生成客户端Stub,让客户端通过Stub进行远程调用,就跟调用本地的方法一样。

服务发现问题

最后解决第三个问题,服务发现问题。

这里有一个UDDI(Universal Description, Discovery, and Integration),也即统一描述、发现和集成协议。它其实是一个注册中心,服务提供方可以将上面的WSDL描述文件,发布到这个注册中心,注册完毕后,服务使用方可以查找到服务的描述,封装为本地的客户端进行调用。

小结

好了,这一节就到这里了,我们来总结一下。

  • 原来的二进制RPC有很多缺点,格式要求严格,修改过于复杂,不面向对象,于是产生了基于文本的调用方式——基于XML的SOAP。
  • SOAP有三大要素:协议约定用WSDL、传输协议用HTTP、服务发现用UDDL。

协议约定问题

然而RESTful可不仅仅是指API,而是一种架构风格,全称Representational State Transfer,表述性状态转移,来自一篇重要的论文《架构风格与基于网络的软件架构设计》(Architectural Styles and the Design of Network-based Software Architectures)。

这篇文章从深层次,更加抽象地论证了一个互联网应用应该有的设计要点,而这些设计要点,成为后来我们能看到的所有高并发应用设计都必须要考虑的问题,再加上REST API比较简单直接,所以后来几乎成为互联网应用的标准接口。

因此,和SOAP不一样,REST不是一种严格规定的标准,它其实是一种设计风格。如果按这种风格进行设计,RESTful接口和SOAP接口都能做到,只不过后面的架构是REST倡导的,而SOAP相对比较关注前面的接口。

而且由于能够通过WSDL生成客户端的Stub,因而SOAP常常被用于类似传统的RPC方式,也即调用远端和调用本地是一样的。

然而本地调用和远程跨网络调用毕竟不一样,这里的不一样还不仅仅是因为有网络而导致的客户端和服务端的分离,从而带来的网络性能问题。更重要的问题是,客户端和服务端谁来维护状态。所谓的状态就是对某个数据当前处理到什么程度了。

这里举几个例子,例如,我浏览到哪个目录了,我看到第几页了,我要买个东西,需要扣减一下库存,这些都是状态。本地调用其实没有人纠结这个问题,因为数据都在本地,谁处理都一样,而且一边处理了,另一边马上就能看到。

当有了RPC之后,我们本来期望对上层透明,就像上一节说的“远在天边,尽在眼前”。于是使用RPC的时候,对于状态的问题也没有太多的考虑。

就像NFS一样,客户端会告诉服务端,我要进入哪个目录,服务端必须要为某个客户端维护一个状态,就是当前这个客户端浏览到哪个目录了。例如,客户端输入cd hello,服务端要在某个地方记住,上次浏览到/root/liuchao了,因而客户的这次输入,应该给它显示/root/liuchao/hello下面的文件列表。而如果有另一个客户端,同样输入cd hello,服务端也在某个地方记住,上次浏览到/var/lib,因而要给客户显示的是/var/lib/hello。

不光NFS,如果浏览翻页,我们经常要实现函数next(),在一个列表中取下一页,但是这就需要服务端记住,客户端A上次浏览到20~30页了,那它调用next(),应该显示30~40页,而客户端B上次浏览到100~110页了,调用next()应该显示110~120页。

上面的例子都是在RPC场景下,由服务端来维护状态,很多SOAP接口设计的时候,也常常按这种模式。这种模式原来没有问题,是因为客户端和服务端之间的比例没有失衡。因为一般不会同时有太多的客户端同时连上来,所以NFS还能把每个客户端的状态都记住。

公司内部使用的ERP系统,如果使用SOAP的方式实现,并且服务端为每个登录的用户维护浏览到报表那一页的状态,由于一个公司内部的人也不会太多,把ERP放在一个强大的物理机上,也能记得过来。

但是互联网场景下,客户端和服务端就彻底失衡了。你可以想象“双十一”,多少人同时来购物,作为服务端,它能记得过来吗?当然不可能,只好多个服务端同时提供服务,大家分担一下。但是这就存在一个问题,服务端怎么把自己记住的客户端状态告诉另一个服务端呢?或者说,你让我给你分担工作,你也要把工作的前因后果给我说清楚啊!

那服务端索性就要想了,既然这么多客户端,那大家就分分工吧。服务端就只记录资源的状态,例如文件的状态,报表的状态,库存的状态,而客户端自己维护自己的状态。比如,你访问到哪个目录了啊,报表的哪一页了啊,等等。

这样对于API也有影响,也就是说,当客户端维护了自己的状态,就不能这样调用服务端了。例如客户端说,我想访问当前目录下的hello路径。服务端说,我怎么知道你的当前路径。所以客户端要先看看自己当前路径是/root/liuchao,然后告诉服务端说,我想访问/root/liuchao/hello路径。

再比如,客户端说我想访问下一页,服务端说,我怎么知道你当前访问到哪一页了。所以客户端要先看看自己访问到了100~110页,然后告诉服务器说,我想访问110~120页。

这就是服务端的无状态化。这样服务端就可以横向扩展了,一百个人一起服务,不用交接,每个人都能处理。

所谓的无状态,其实是服务端维护资源的状态,客户端维护会话的状态。对于服务端来讲,只有资源的状态改变了,客户端才调用POST、PUT、DELETE方法来找我;如果资源的状态没变,只是客户端的状态变了,就不用告诉我了,对于我来说都是统一的GET。

虽然这只改进了GET,但是已经带来了很大的进步。因为对于互联网应用,大多数是读多写少的。而且只要服务端的资源状态不变,就给了我们缓存的可能。例如可以将状态缓存到接入层,甚至缓存到CDN的边缘节点,这都是资源状态不变的好处。

按照这种思路,对于API的设计,就慢慢变成了以资源为核心,而非以过程为核心。也就是说,客户端只要告诉服务端你想让资源状态最终变成什么样就可以了,而不用告诉我过程,不用告诉我动作。

还是文件目录的例子。客户端应该访问哪个绝对路径,而非一个动作,我就要进入某个路径。再如,库存的调用,应该查看当前的库存数目,然后减去购买的数量,得到结果的库存数。这个时候应该设置为目标库存数(但是当前库存数要匹配),而非告知减去多少库存。

这种API的设计需要实现幂等,因为网络不稳定,就会经常出错,因而需要重试,但是一旦重试,就会存在幂等的问题,也就是同一个调用,多次调用的结果应该一样,不能一次支付调用,因为调用三次变成了支付三次。不能进入cd a,做了三次,就变成了cd a/a/a。也不能扣减库存,调用了三次,就扣减三次库存。

当然按照这种设计模式,无论RESTful API还是SOAP API都可以将架构实现成无状态的,面向资源的、幂等的、横向扩展的、可缓存的。

但是SOAP的XML正文中,是可以放任何动作的。例如XML里面可以写< ADD >,< MINUS >等。这就方便使用SOAP的人,将大量的动作放在API里面。

RESTful没这么复杂,也没给客户提供这么多的可能性,正文里的JSON基本描述的就是资源的状态,没办法描述动作,而且能够出发的动作只有CRUD,也即POST、GET、PUT、DELETE,也就是对于状态的改变。

所以,从接口角度,就让你死了这条心。当然也有很多技巧的方法,在使用RESTful API的情况下,依然提供基于动作的有状态请求,这属于反模式了。

服务发现问题

对于RESTful API来讲,我们已经解决了传输协议的问题——基于HTTP,协议约定问题——基于JSON,最后要解决的是服务发现问题。

有个著名的基于RESTful API的跨系统调用框架叫Spring Cloud。在Spring Cloud中有一个组件叫 Eureka。传说,阿基米德在洗澡时发现浮力原理,高兴得来不及穿上裤子,跑到街上大喊:“Eureka(我找到了)!”所以Eureka是用来实现注册中心的,负责维护注册的服务列表。

服务分服务提供方,它向Eureka做服务注册、续约和下线等操作,注册的主要数据包括服务名、机器IP、端口号、域名等等。

另外一方是服务消费方,向Eureka获取服务提供方的注册信息。为了实现负载均衡和容错,服务提供方可以注册多个。

当消费方要调用服务的时候,会从注册中心读出多个服务来,那怎么调用呢?当然是RESTful方式了。

Spring Cloud提供一个RestTemplate工具,用于将请求对象转换为JSON,并发起Rest调用,RestTemplate的调用也是分POST、PUT、GET、 DELETE的,当结果返回的时候,根据返回的JSON解析成对象。

通过这样封装,调用起来也很方便。

小结

好了,这一节就到这里了,我们来总结一下。

  • SOAP过于复杂,而且设计是面向动作的,因而往往因为架构问题导致并发量上不去。
  • RESTful不仅仅是一个API,而且是一种架构模式,主要面向资源,提供无状态服务,有利于横向扩展应对高并发。

在讲CDN和DNS的时候,我们讲过接入层的设计,对于静态资源或者动态资源静态化的部分都可以做缓存。但是对于下单、支付等交易场景,还是需要调用API。

对于微服务的架构,API需要一个API网关统一的管理。API网关有多种实现方式,用Nginx或者OpenResty结合Lua脚本是常用的方式。在上一节讲过的Spring Cloud体系中,有个组件Zuul也是干这个的。

数据中心内部是如何相互调用的?

API网关用来管理API,但是API的实现一般在一个叫作Controller层的地方。这一层对外提供API。由于是让陌生人访问的,我们能看到目前业界主流的,基本都是RESTful的API,是面向大规模互联网应用的。

在Controller之内,就是咱们互联网应用的业务逻辑实现。上节讲RESTful的时候,说过业务逻辑的实现最好是无状态的,从而可以横向扩展,但是资源的状态还需要服务端去维护。资源的状态不应该维护在业务逻辑层,而是在最底层的持久化层,一般会使用分布式数据库和ElasticSearch。

这些服务端的状态,例如订单、库存、商品等,都是重中之重,都需要持久化到硬盘上,数据不能丢,但是由于硬盘读写性能差,因而持久化层往往吞吐量不能达到互联网应用要求的吞吐量,因而前面要有一层缓存层,使用Redis或者memcached将请求拦截一道,不能让所有的请求都进入数据库“中军大营”。

缓存和持久化层之上一般是基础服务层,这里面提供一些原子化的接口。例如,对于用户、商品、订单、库存的增删查改,将缓存和数据库对再上层的业务逻辑屏蔽一道。有了这一层,上层业务逻辑看到的都是接口,而不会调用数据库和缓存。因而对于缓存层的扩容,数据库的分库分表,所有的改变,都截止到这一层,这样有利于将来对于缓存和数据库的运维。

再往上就是组合层。因为基础服务层只是提供简单的接口,实现简单的业务逻辑,而复杂的业务逻辑,比如下单,要扣优惠券,扣减库存等,就要在组合服务层实现。

这样,Controller层、组合服务层、基础服务层就会相互调用,这个调用是在数据中心内部的,量也会比较大,还是使用RPC的机制实现的。

由于服务比较多,需要一个单独的注册中心来做服务发现。服务提供方会将自己提供哪些服务注册到注册中心中去,同时服务消费方订阅这个服务,从而可以对这个服务进行调用。

调用的时候有一个问题,这里的RPC调用,应该用二进制还是文本类?其实文本的最大问题是,占用字节数目比较多。比如数字123,其实本来二进制8位就够了,但是如果变成文本,就成了字符串123。如果是UTF-8编码的话,就是三个字节;如果是UTF-16,就是六个字节。同样的信息,要多费好多的空间,传输起来也更加占带宽,时延也高。

因而对于数据中心内部的相互调用,很多公司选型的时候,还是希望采用更加省空间和带宽的二进制的方案。

这里一个著名的例子就是Dubbo服务化框架二进制的RPC方式。

Dubbo会在客户端的本地启动一个Proxy,其实就是客户端的Stub,对于远程的调用都通过这个Stub进行封装。

接下来,Dubbo会从注册中心获取服务端的列表,根据路由规则和负载均衡规则,在多个服务端中选择一个最合适的服务端进行调用。

调用服务端的时候,首先要进行编码和序列化,形成Dubbo头和序列化的方法和参数。将编码好的数据,交给网络客户端进行发送,网络服务端收到消息后,进行解码。然后将任务分发给某个线程进行处理,在线程中会调用服务端的代码逻辑,然后返回结果。

这个过程和经典的RPC模式何其相似啊!

如何解决协议约定问题?

接下来我们还是来看RPC的三大问题,其中注册发现问题已经通过注册中心解决了。我们下面就来看协议约定问题。

Dubbo中默认的RPC协议是Hessian2。为了保证传输的效率,Hessian2将远程调用序列化为二进制进行传输,并且可以进行一定的压缩。这个时候你可能会疑惑,同为二进制的序列化协议,Hessian2和前面的二进制的RPC有什么区别呢?这不绕了一圈又回来了吗?

Hessian2是解决了一些问题的。例如,原来要定义一个协议文件,然后通过这个文件生成客户端和服务端的Stub,才能进行相互调用,这样使得修改就会不方便。Hessian2不需要定义这个协议文件,而是自描述的。什么是自描述呢?

所谓自描述就是,关于调用哪个函数,参数是什么,另一方不需要拿到某个协议文件、拿到二进制,靠它本身根据Hessian2的规则,就能解析出来。

原来有协议文件的场景,有点儿像两个人事先约定好,0表示方法add,然后后面会传两个数。服务端把两个数加起来,这样一方发送012,另一方知道是将1和2加起来,但是不知道协议文件的,当它收到012的时候,完全不知道代表什么意思。

而自描述的场景,就像两个人说的每句话都带前因后果。例如,传递的是“函数:add,第一个参数1,第二个参数2”。这样无论谁拿到这个表述,都知道是什么意思。但是只不过都是以二进制的形式编码的。这其实相当于综合了XML和二进制共同优势的一个协议。

看起来很复杂,编译原理里面是有这样的语法规则的。

我们从Top看起,下一层是value,直到形成一棵树。这里面的有个思想,为了防止歧义,每一个类型的起始数字都设置成为独一无二的。这样,解析的时候,看到这个数字,就知道后面跟的是什么了。

这里还是以加法为例子,“add(2,3)”被序列化之后是什么样的呢?

1
2
3
4
5
6
H x02 x00     # Hessian 2.0
C # RPC call
x03 add # method "add"
x92 # two arguments
x92 # 2 - argument 1
x93 # 3 - argument 2
  • H开头,表示使用的协议是Hession,H的二进制是0x48。
  • C开头,表示这是一个RPC调用。
  • 0x03,表示方法名是三个字符。
  • 0x92,表示有两个参数。其实这里存的应该是2,之所以加上0x90,就是为了防止歧义,表示这里一定是一个int。
  • 第一个参数是2,编码为0x92,第二个参数是3,编码为0x93。

这个就叫作自描述

另外,Hessian2是面向对象的,可以传输一个对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Car {
String color;
String model;
}
out.writeObject(new Car("red", "corvette"));
out.writeObject(new Car("green", "civic"));
---
C # object definition (#0)
x0b example.Car # type is example.Car
x92 # two fields
x05 color # color field name
x05 model # model field name

O # object def (long form)
x90 # object definition #0
x03 red # color field value
x08 corvette # model field value

x60 # object def #0 (short form)
x05 green # color field value
x05 civic # model field value

首先,定义这个类。对于类型的定义也传过去,因而也是自描述的。类名为example.Car,字符长11位,因而前面长度为0x0b。有两个成员变量,一个是color,一个是model,字符长5位,因而前面长度0x05,。

然后,传输的对象引用这个类。由于类定义在位置0,因而对象会指向这个位置0,编码为0x90。后面red和corvette是两个成员变量的值,字符长分别为3和8。

接着又传输一个属于相同类的对象。这时候就不保存对于类的引用了,只保存一个0x60,表示同上就可以了。

可以看出,Hessian2真的是能压缩尽量压缩,多一个Byte都不传。

如何解决RPC传输问题?

接下来,我们再来看Dubbo的RPC传输问题。前面我们也说了,基于Socket实现一个高性能的服务端,是很复杂的一件事情,在Dubbo里面,使用了Netty的网络传输框架。

Netty是一个非阻塞的基于事件的网络传输框架,在服务端启动的时候,会监听一个端口,并注册以下的事件。

  • 连接事件:当收到客户端的连接事件时,会调用void connected(Channel channel) 方法。
  • 可写事件触发时,会调用void sent(Channel channel, Object message),服务端向客户端返回响应数据。
  • 可读事件触发时,会调用void received(Channel channel, Object message) ,服务端在收到客户端的请求数据。
  • 发生异常时,会调用void caught(Channel channel, Throwable exception)。

当事件触发之后,服务端在这些函数中的逻辑,可以选择直接在这个函数里面进行操作,还是将请求分发到线程池去处理。一般异步的数据读写都需要另外的线程池参与,在线程池中会调用真正的服务端业务代码逻辑,返回结果。

Hessian2是Dubbo默认的RPC序列化方式,当然还有其他选择。例如,Dubbox从Spark那里借鉴Kryo,实现高性能的序列化。

到这里,我们说了数据中心里面的相互调用。为了高性能,大家都愿意用二进制,但是为什么后期Spring Cloud又兴起了呢?这是因为,并发量越来越大,已经到了微服务的阶段。同原来的SOA不同,微服务粒度更细,模块之间的关系更加复杂。

在上面的架构中,如果使用二进制的方式进行序列化,虽然不用协议文件来生成Stub,但是对于接口的定义,以及传的对象DTO,还是需要共享JAR。因为只有客户端和服务端都有这个JAR,才能成功地序列化和反序列化。

但当关系复杂的时候,JAR的依赖也变得异常复杂,难以维护,而且如果在DTO里加一个字段,双方的JAR没有匹配好,也会导致序列化不成功,而且还有可能循环依赖。这个时候,一般有两种选择。

第一种,建立严格的项目管理流程。

  • 不允许循环调用,不允许跨层调用,只准上层调用下层,不允许下层调用上层。
  • 接口要保持兼容性,不兼容的接口新添加而非改原来的,当接口通过监控,发现不用的时候,再下掉。
  • 升级的时候,先升级服务提供端,再升级服务消费端。

第二种,改用RESTful的方式。

  • 使用Spring Cloud,消费端和提供端不用共享JAR,各声明各的,只要能变成JSON就行,而且JSON也是比较灵活的。
  • 使用RESTful的方式,性能会降低,所以需要通过横向扩展来抵消单机的性能损耗。

这个时候,就看架构师的选择喽!

小结

好了,这节就到这里了,我们来总结一下。

  • RESTful API对于接入层和Controller层之外的调用,已基本形成事实标准,但是随着内部服务之间的调用越来越多,性能也越来越重要,于是Dubbo的RPC框架有了用武之地。
  • Dubbo通过注册中心解决服务发现问题,通过Hessian2序列化解决协议约定的问题,通过Netty解决网络传输的问题。
  • 在更加复杂的微服务场景下,Spring Cloud的RESTful方式在内部调用也会被考虑,主要是JAR包的依赖和管理问题。

到目前为止,咱们讲了四种RPC,分别是ONC RPC、基于XML的SOAP、基于JSON的RESTful和Hessian2。

通过学习,我们知道,二进制的传输性能好,文本类的传输性能差一些;二进制的难以跨语言,文本类的可以跨语言;要写协议文件的严谨一些,不写协议文件的灵活一些。虽然都有服务发现机制,有的可以进行服务治理,有的则没有。

我们也看到了RPC从最初的客户端服务器模式,最终演进到微服务。对于RPC框架的要求越来越多了,具体有哪些要求呢?

  • 首先,传输性能很重要。因为服务之间的调用如此频繁了,还是二进制的越快越好。
  • 其次,跨语言很重要。因为服务多了,什么语言写成的都有,而且不同的场景适宜用不同的语言,不能一个语言走到底。
  • 最好既严谨又灵活,添加个字段不用重新编译和发布程序。
  • 最好既有服务发现,也有服务治理,就像Dubbo和Spring Cloud一样。

Protocol Buffers

这是要多快好省的建设社会主义啊。理想还是要有的嘛,这里我就来介绍一个向“理想”迈进的GRPC。

GRPC首先满足二进制和跨语言这两条,二进制说明压缩效率高,跨语言说明更灵活。但是又是二进制,又是跨语言,这就相当于两个人沟通,你不但说方言,还说缩略语,人家怎么听懂呢?所以,最好双方弄一个协议约定文件,里面规定好双方沟通的专业术语,这样沟通就顺畅多了。

对于GRPC来讲,二进制序列化协议是Protocol Buffers。首先,需要定义一个协议文件.proto。

在这个协议文件中,我们首先指定使用proto3的语法,然后我们使用Protocol Buffers的语法,定义两个消息的类型,一个是发出去的参数,一个是返回的结果。里面的每一个字段,例如date、classname、author、price都有唯一的一个数字标识,这样在压缩的时候,就不用传输字段名称了,只传输这个数字标识就行了,能节省很多空间。

最后定义一个Service,里面会有一个RPC调用的声明。

无论使用什么语言,都有相应的工具生成客户端和服务端的Stub程序,这样客户端就可以像调用本地一样,调用远程的服务了。

协议约定问题

Protocol Buffers是一款压缩效率极高的序列化协议,有很多设计精巧的序列化方法。

对于int类型32位的,一般都需要4个Byte进行存储。在Protocol Buffers中,使用的是变长整数的形式。对于每一个Byte的8位,最高位都有特殊的含义。

如果该位为 1,表示这个数字没完,后续的Byte也属于这个数字;如果该位为 0,则这个数字到此结束。其他的7个Bit才是用来表示数字的内容。因此,小于128的数字都可以用一个Byte表示;大于128的数字,比如130,会用两个字节来表示。

对于每一个字段,使用的是TLV(Tag,Length,Value)的存储办法。

其中Tag = (field_num << 3) | wire_type。field_num就是在proto文件中,给每个字段指定唯一的数字标识,而wire_type用于标识后面的数据类型。

如果我们想修改协议文件,对于赋给某个标签的数字,例如string author=3,这个就不要改变了,改变了就不认了;也不要添加或者删除required字段,因为解析的时候,发现没有这个字段就会报错。对于optional和repeated字段,可以删除,也可以添加。这就给了客户端和服务端升级的可能性。

例如,我们在协议里面新增一个string recommended字段,表示这个课程是谁推荐的,就将这个字段设置为optional。我们可以先升级服务端,当客户端发过来消息的时候,是没有这个值的,将它设置为一个默认值。我们也可以先升级客户端,当客户端发过来消息的时候,是有这个值的,那它将被服务端忽略。

至此,我们解决了协议约定的问题。

网络传输问题

接下来,我们来看网络传输的问题。

如果是Java技术栈,GRPC的客户端和服务器之间通过Netty Channel作为数据通道,每个请求都被封装成HTTP 2.0的Stream。

Netty是一个高效的基于异步IO的网络传输框架,这个上一节我们已经介绍过了。HTTP 2.0在第14讲,我们也介绍过。HTTP 2.0协议将一个TCP的连接,切分成多个流,每个流都有自己的ID,而且流是有优先级的。流可以是客户端发往服务端,也可以是服务端发往客户端。它其实只是一个虚拟的通道。

HTTP 2.0还将所有的传输信息分割为更小的消息和帧,并对它们采用二进制格式编码。

通过这两种机制,HTTP 2.0的客户端可以将多个请求分到不同的流中,然后将请求内容拆成帧,进行二进制传输。这些帧可以打散乱序发送, 然后根据每个帧首部的流标识符重新组装,并且可以根据优先级,决定优先处理哪个流的数据。

由于基于HTTP 2.0,GRPC和其他的RPC不同,可以定义四种服务方法。

第一种,也是最常用的方式是单向RPC,即客户端发送一个请求给服务端,从服务端获取一个应答,就像一次普通的函数调用。

1
rpc SayHello(HelloRequest) returns (HelloResponse){}

第二种方式是服务端流式RPC,即服务端返回的不是一个结果,而是一批。客户端发送一个请求给服务端,可获取一个数据流用来读取一系列消息。客户端从返回的数据流里一直读取,直到没有更多消息为止。

1
rpc LotsOfReplies(HelloRequest) returns (stream HelloResponse){}

第三种方式为客户端流式RPC,也即客户端的请求不是一个,而是一批。客户端用提供的一个数据流写入并发送一系列消息给服务端。一旦客户端完成消息写入,就等待服务端读取这些消息并返回应答。

1
rpc LotsOfGreetings(stream HelloRequest) returns (HelloResponse) {}

第四种方式为双向流式 RPC,即两边都可以分别通过一个读写数据流来发送一系列消息。这两个数据流操作是相互独立的,所以客户端和服务端能按其希望的任意顺序读写,服务端可以在写应答前等待所有的客户端消息,或者它可以先读一个消息再写一个消息,或者读写相结合的其他方式。每个数据流里消息的顺序会被保持。

1
rpc BidiHello(stream HelloRequest) returns (stream HelloResponse){}

如果基于HTTP 2.0,客户端和服务器之间的交互方式要丰富得多,不仅可以单方向远程调用,还可以实现当服务端状态改变的时候,主动通知客户端。

至此,传输问题得到了解决。

服务发现与治理问题

最后是服务发现与服务治理的问题。

GRPC本身没有提供服务发现的机制,需要借助其他的组件,发现要访问的服务端,在多个服务端之间进行容错和负载均衡。

其实负载均衡本身比较简单,LVS、HAProxy、Nginx都可以做,关键问题是如何发现服务端,并根据服务端的变化,动态修改负载均衡器的配置。

在这里我们介绍一种对于GRPC支持比较好的负载均衡器Envoy。其实Envoy不仅仅是负载均衡器,它还是一个高性能的C++写的Proxy转发器,可以配置非常灵活的转发规则。

这些规则可以是静态的,放在配置文件中的,在启动的时候加载。要想重新加载,一般需要重新启动,但是Envoy支持热加载和热重启,这在一定程度上缓解了这个问题。

当然,最好的方式是将规则设置为动态的,放在统一的地方维护。这个统一的地方在Envoy眼中被称为服务发现(Discovery Service),过一段时间去这里拿一下配置,就修改了转发策略。

无论是静态的,还是动态的,在配置里面往往会配置四个东西。

第一个是listener。Envoy既然是Proxy,专门做转发,就得监听一个端口,接入请求,然后才能够根据策略转发,这个监听的端口就称为listener。

第二个是endpoint,是目标的IP地址和端口。这个是Proxy最终将请求转发到的地方。

第三个是cluster。一个cluster是具有完全相同行为的多个endpoint,也即如果有三个服务端在运行,就会有三个IP和端口,但是部署的是完全相同的三个服务,它们组成一个cluster,从cluster到endpoint的过程称为负载均衡,可以轮询。

第四个是route。有时候多个cluster具有类似的功能,但是是不同的版本号,可以通过route规则,选择将请求路由到某一个版本号,也即某一个cluster。

如果是静态的,则将后端的服务端的IP地址拿到,然后放在配置文件里面就可以了。

如果是动态的,就需要配置一个服务发现中心,这个服务发现中心要实现Envoy的API,Envoy可以主动去服务发现中心拉取转发策略。

看来,Envoy进程和服务发现中心之间要经常相互通信,互相推送数据,所以Envoy在控制面和服务发现中心沟通的时候,就可以使用GRPC,也就天然具备在用户面支撑GRPC的能力。

Envoy如果复杂的配置,都能干什么事呢?

一种常见的规则是配置路由策略。例如后端的服务有两个版本,可以通过配置Envoy的route,来设置两个版本之间,也即两个cluster之间的route规则,一个占99%的流量,一个占1%的流量。

另一种常见的规则就是负载均衡策略。对于一个cluster下的多个endpoint,可以配置负载均衡机制和健康检查机制,当服务端新增了一个,或者挂了一个,都能够及时配置Envoy,进行负载均衡。

所有这些节点的变化都会上传到注册中心,所有这些策略都可以通过注册中心进行下发,所以,更严格的意义上讲,注册中心可以称为注册治理中心

Envoy这么牛,是不是能够将服务之间的相互调用全部由它代理?如果这样,服务也不用像Dubbo,或者Spring Cloud一样,自己感知到注册中心,自己注册,自己治理,对应用干预比较大。

如果我们的应用能够意识不到服务治理的存在,就是直接进行GRPC的调用就可以了。

这就是未来服务治理的趋势Serivce Mesh,也即应用之间的相互调用全部由Envoy进行代理,服务之间的治理也被Envoy进行代理,完全将服务治理抽象出来,到平台层解决。

至此RPC框架中有治理功能的Dubbo、Spring Cloud、Service Mesh就聚齐了。

小结

好了,这一节就到这里了,我们来总结一下。

  • GRPC是一种二进制,性能好,跨语言,还灵活,同时可以进行服务治理的多快好省的RPC框架,唯一不足就是还是要写协议文件。
  • GRPC序列化使用Protocol Buffers,网络传输使用HTTP 2.0,服务治理可以使用基于Envoy的Service Mesh。

接下来,你在云计算的界面上创建一个VPC(Virtual Private Cloud,虚拟私有网络),指定一个IP段,这样以后你部署的所有应用都会在这个虚拟网络里,使用你分配的这个IP段。为了不同的VPC相互隔离,每个VPC都会被分配一个VXLAN的ID。尽管不同用户的虚拟机有可能在同一个物理机上,但是不同的VPC二层压根儿是不通的。

由于有两个可用区,在这个VPC里面,要为每一个可用区分配一个Subnet,也就是在大的网段里分配两个小的网段。当两个可用区里面网段不同的时候,就可以配置路由策略,访问另外一个可用区,走某一条路由了。

接下来,应该创建数据库持久化层。大部分云平台都会提供PaaS服务,也就是说,不需要你自己搭建数据库,而是采用直接提供数据库的服务,并且单机房的主备切换都是默认做好的,数据库也是部署在虚拟机里面的,只不过从界面上,你看不到数据库所在的虚拟机而已。

云平台会给每个Subnet的数据库实例分配一个域名。创建数据库实例的时候,需要你指定可用区和Subnet,这样创建出来的数据库实例可以通过这个Subnet的私网IP进行访问。

为了分库分表实现高并发的读写,在创建的多个数据库实例之上,会创建一个分布式数据库的实例,也需要指定可用区和Subnet,还会为分布式数据库分配一个私网IP和域名。

对于数据库这种高可用性比较高的,需要进行跨机房高可用,因而两个可用区都要部署一套,但是只有一个是主,另外一个是备,云平台往往会提供数据库同步工具,将应用写入主的数据同步给备数据库集群。

接下来是创建缓存集群。云平台也会提供PaaS服务,也需要每个可用区和Subnet创建一套,缓存的数据在内存中,由于读写性能要求高,一般不要求跨可用区读写。

再往上层就是部署咱们自己写的程序了。基础服务层、组合服务层、Controller层,以及Nginx层、API网关等等,这些都是部署在虚拟机里面的。它们之间通过RPC相互调用,需要到注册中心进行注册。

它们之间的网络通信是虚拟机和虚拟机之间的。如果是同一台物理机,则那台物理机上的OVS就能转发过去;如果是不同的物理机,这台物理机的OVS和另一台物理机的OVS中间有一个VXLAN的隧道,将请求转发过去。

再往外就是负载均衡了,负载均衡也是云平台提供的PaaS服务,也是属于某个VPC的,部署在虚拟机里面的,但是负载均衡有个外网的IP,这个外网的IP地址就是在网关节点的外网网口上的。在网关节点上,会有NAT规则,将外网IP地址转换为VPC里面的私网IP地址,通过这些私网IP地址访问到虚拟机上的负载均衡节点,然后通过负载均衡节点转发到API网关的节点。

网关节点的外网网口是带公网IP地址的,里面有一个虚拟网关转发模块,还会有一个OVS,将私网IP地址放到VXLAN隧道里面,转发到虚拟机上,从而实现外网和虚拟机网络之间的互通。

不同的可用区之间,通过核心交换机连在一起,核心交换机之外是边界路由器。

在华北、华东、华南同样也部署了一整套,每个地区都创建了VPC,这就需要有一种机制将VPC连接到一起。云平台一般会提供硬件的VPC互连的方式,当然也可以使用软件互连的方式,也就是使用VPN网关,通过IPsec VPN将不同地区的不同VPC通过VPN连接起来。

对于不同地区和不同运营商的用户,我们希望他能够就近访问到网站,而且当一个点出了故障之后,我们希望能够在不同的地区之间切换,这就需要有智能DNS,这个也是云平台提供的。

对于一些静态资源,可以保持在对象存储里面,通过CDN下发到边缘节点,这样客户端就能尽快加载出来。

2.大声告诉全世界,可以到我这里买东西

当电商应用搭建完毕之后,接下来需要将如何访问到这个电商网站广播给全网。

刚才那张图画的是一个可用区的情况,对于多个可用区的情况,我们可以隐去计算节点的情况,将外网访问区域放大。

外网IP是放在虚拟网关的外网网口上的,这个IP如何让全世界知道呢?当然是通过BGP路由协议了。

每个可用区都有自己的汇聚交换机,如果机器数目比较多,可以直接用核心交换机,每个Region也有自己的核心交换区域。

在核心交换外面是安全设备,然后就是边界路由器。边界路由器会和多个运营商连接,从而每个运营商都能够访问到这个网站。边界路由器可以通过BGP协议,将自己数据中心里面的外网IP向外广播,也就是告诉全世界,如果要访问这些外网IP,都来我这里。

每个运营商也有很多的路由器、很多的点,于是就可以将如何到达这些IP地址的路由信息,广播到全国乃至全世界。

3.打开手机来上网,域名解析得地址

这个时候,不但你的这个网站的IP地址全世界都知道了,你打的广告可能大家也都看到了,于是有客户下载App来买东西了。

客户的手机开机以后,在附近寻找基站eNodeB,发送请求,申请上网。基站将请求发给MME,MME对手机进行认证和鉴权,还会请求HSS看有没有钱,看看是在哪里上网。

当MME通过了手机的认证之后,开始建立隧道,建设的数据通路分两段路,其实是两个隧道。一段是从eNodeB到SGW,第二段是从SGW到PGW,在PGW之外,就是互联网。

PGW会为手机分配一个IP地址,手机上网都是带着这个IP地址的。

当在手机上面打开一个App的时候,首先要做的事情就是解析这个网站的域名。

在手机运营商所在的互联网区域里,有一个本地的DNS,手机会向这个DNS请求解析DNS。当这个DNS本地有缓存,则直接返回;如果没有缓存,本地DNS才需要递归地从根DNS服务器,查到.com的顶级域名服务器,最终查到权威DNS服务器。

如果你使用云平台的时候,配置了智能DNS和全局负载均衡,在权威DNS服务中,一般是通过配置CNAME的方式,我们可以起一个别名,例如vip.yourcomany.com ,然后告诉本地DNS服务器,让它请求GSLB解析这个域名,GSLB就可以在解析这个域名的过程中,通过自己的策略实现负载均衡。

GSLB通过查看请求它的本地DNS服务器所在的运营商和地址,就知道用户所在的运营商和地址,然后将距离用户位置比较近的Region里面,三个负载均衡SLB的公网IP地址,返回给本地DNS服务器。本地DNS解析器将结果缓存后,返回给客户端。

对于手机App来说,可以绕过刚才的传统DNS解析机制,直接只要HTTPDNS服务,通过直接调用HTTPDNS服务器,得到这三个SLB的公网IP地址。

我们部署电商应用的时候,一般会把静态资源保存在两个地方,一个是接入层nginx后面的varnish缓存里面,一般是静态页面;对于比较大的、不经常更新的静态图片,会保存在对象存储里面。这两个地方的静态资源都会配置CDN,将资源下发到边缘节点。

配置了CDN之后,权威DNS服务器上,会为静态资源设置一个CNAME别名,指向另外一个域名 cdn.com ,返回给本地DNS服务器。

当本地DNS服务器拿到这个新的域名时,需要继续解析这个新的域名。这个时候,再访问的时候就不是原来的权威DNS服务器了,而是 cdn.com 的权威DNS服务器。这是CDN自己的权威DNS服务器。

在这个服务器上,还是会设置一个CNAME,指向另外一个域名,也即CDN网络的全局负载均衡器。

本地DNS服务器去请求CDN的全局负载均衡器解析域名,全局负载均衡器会为用户选择一台合适的缓存服务器提供服务,将IP返回给客户端,客户端去访问这个边缘节点,下载资源。缓存服务器响应用户请求,将用户所需内容传送到用户终端。

如果这台缓存服务器上并没有用户想要的内容,那么这台服务器就要向它的上一级缓存服务器请求内容,直至追溯到网站的源服务器,将内容拉到本地。

电商网站会对下单的情况提供RESTful的下单接口,而对于下单这种需要保密的操作,需要通过HTTPS协议进行请求。

在所有这些操作之前,首先要做的事情是建立连接

HTTPS协议是基于TCP协议的,因而要先建立TCP的连接。在这个例子中,TCP的连接是从手机上的App和负载均衡器SLB之间的。

尽管中间要经过很多的路由器和交换机,但是TCP的连接是端到端的。TCP这一层和更上层的HTTPS无法看到中间的包的过程。尽管建立连接的时候,所有的包都逃不过在这些路由器和交换机之间的转发,转发的细节我们放到那个下单请求的发送过程中详细解读,这里只看端到端的行为。

对于TCP连接来讲,需要通过三次握手建立连接,为了维护这个连接,双方都需要在TCP层维护一个连接的状态机。

一开始,客户端和服务端都处于CLOSED状态。服务端先是主动监听某个端口,处于LISTEN状态。然后客户端主动发起连接SYN,之后处于SYN-SENT状态。服务端收到发起的连接,返回SYN,并且ACK客户端的SYN,之后处于SYN-RCVD状态。

客户端收到服务端发送的SYN和ACK之后,发送ACK的ACK,之后处于ESTABLISHED状态。这是因为,它一发一收成功了。服务端收到ACK的ACK之后,处于ESTABLISHED状态,因为它的一发一收也成功了。

当TCP层的连接建立完毕之后,接下来轮到HTTPS层建立连接了,在HTTPS的交换过程中,TCP层始终处于ESTABLISHED。

对于HTTPS,客户端会发送Client Hello消息到服务器,用明文传输TLS版本信息、加密套件候选列表、压缩算法候选列表等信息。另外,还会有一个随机数,在协商对称密钥的时候使用。

然后,服务器会返回Server Hello消息,告诉客户端,服务器选择使用的协议版本、加密套件、压缩算法等。这也有一个随机数,用于后续的密钥协商。

然后,服务器会给你一个服务器端的证书,然后说:“Server Hello Done,我这里就这些信息了。”

客户端当然不相信这个证书,于是从自己信任的CA仓库中,拿CA的证书里面的公钥去解密电商网站的证书。如果能够成功,则说明电商网站是可信的。这个过程中,你可能会不断往上追溯CA、CA的CA、CA的CA的CA,反正直到一个授信的CA,就可以了。

证书验证完毕之后,觉得这个服务端是可信的,于是客户端计算产生随机数字Pre-master,发送Client Key Exchange,用证书中的公钥加密,再发送给服务器,服务器可以通过私钥解密出来。

接下来,无论是客户端还是服务器,都有了三个随机数,分别是:自己的、对端的,以及刚生成的Pre-Master随机数。通过这三个随机数,可以在客户端和服务器产生相同的对称密钥。

有了对称密钥,客户端就可以说:“Change Cipher Spec,咱们以后都采用协商的通信密钥和加密算法进行加密通信了。”

然后客户端发送一个Encrypted Handshake Message,将已经商定好的参数等,采用协商密钥进行加密,发送给服务器用于数据与握手验证。

同样,服务器也可以发送Change Cipher Spec,说:“没问题,咱们以后都采用协商的通信密钥和加密算法进行加密通信了”,并且也发送Encrypted Handshake Message的消息试试。

当双方握手结束之后,就可以通过对称密钥进行加密传输了。

真正的下单请求封装成网络包的发送过程,我们先放一放,我们来接着讲这个网络包的故事。

6.发送下单请求网络包,西行需要出网关

当客户端和服务端之间建立了连接后,接下来就要发送下单请求的网络包了。

在用户层发送的是HTTP的网络包,因为服务端提供的是RESTful API,因而HTTP层发送的就是一个请求。

HTTP的报文大概分为三大部分。第一部分是请求行,第二部分是请求的首部,第三部分才是请求的正文实体

在请求行中,URL就是 www.geektime.com/purchaseOrder ,版本为HTTP 1.1。

请求的类型叫作POST,它需要主动告诉服务端一些信息,而非获取。需要告诉服务端什么呢?一般会放在正文里面。正文可以有各种各样的格式,常见的格式是JSON。

请求行下面就是我们的首部字段。首部是key value,通过冒号分隔。

Content-Type是指正文的格式。例如,我们进行POST的请求,如果正文是JSON,那么我们就应该将这个值设置为JSON。

接下来是正文,这里是一个JSON字符串,里面通过文本的形式描述了,要买一个课程,作者是谁,多少钱。

这样,HTTP请求的报文格式就拼凑好了。接下来浏览器或者移动App会把它交给下一层传输层。

怎么交给传输层呢?也是用Socket进行程序设计。如果用的是浏览器,这些程序不需要你自己写,有人已经帮你写好了;如果在移动APP里面,一般会用一个HTTP的客户端工具来发送,并且帮你封装好。

HTTP协议是基于TCP协议的,所以它使用面向连接的方式发送请求,通过Stream二进制流的方式传给对方。当然,到了TCP层,它会把二进制流变成一个的报文段发送给服务器。

在TCP头里面,会有源端口号和目标端口号,目标端口号一般是服务端监听的端口号,源端口号在手机端,往往是随机分配一个端口号。这个端口号在客户端和服务端用于区分请求和返回,发给那个应用。

在IP头里面,都需要加上自己的地址(即源地址)和它想要去的地方(即目标地址)。当一个手机上线的时候,PGW会给这个手机分配一个IP地址,这就是源地址,而目标地址则是云平台的负载均衡器的外网IP地址。

在IP层,客户端需要查看目标地址和自己是否是在同一个局域网,计算是否是同一个网段,往往需要通过CIDR子网掩码来计算。

对于这个下单场景,目标IP和源IP不会在同一个网段,因而需要发送到默认的网关。一般通过DHCP分配IP地址的时候,同时配置默认网关的IP地址。

但是客户端不会直接使用默认网关的IP地址,而是发送ARP协议,来获取网关的MAC地址,然后将网关MAC作为目标MAC,自己的MAC作为源MAC,放入MAC头,发送出去。

发送的时候可以说是重重关隘,从手机到移动网络、互联网,还要经过多个运营商才能到达数据中心,到了数据中心就进入第二个复杂的过程,从网关到VXLAN隧道,到负载均衡,到Controller层、组合服务层、基础服务层,最终才下单入库。今天,我们就来看这最后一段过程。

7.一座座城池一道道关,流控拥塞与重传

网络包已经组合完毕,接下来我们来看,如何经过一道道城关,到达目标公网IP。

对于手机来讲,默认的网关在PGW上。在移动网络里面,从手机到SGW,到PGW是有一条隧道的。在这条隧道里面,会将上面的这个包作为隧道的乘客协议放在里面,外面SGW和PGW在核心网机房的IP地址。网络包直到PGW(PGW是隧道的另一端)才将里面的包解出来,转发到外部网络。

所以,从手机发送出来的时候,网络包的结构为:

  • 源MAC:手机也即UE的MAC;
  • 目标MAC:网关PGW上面的隧道端点的MAC;
  • 源IP:UE的IP地址;
  • 目标IP:SLB的公网IP地址。

进入隧道之后,要封装外层的网络地址,因而网络包的格式为:

  • 外层源MAC:E-NodeB的MAC;
  • 外层目标MAC:SGW的MAC;
  • 外层源IP:E-NodeB的IP;
  • 外层目标IP:SGW的IP;
  • 内层源MAC:手机也即UE的MAC;
  • 内层目标MAC:网关PGW上面的隧道端点的MAC;
  • 内层源IP:UE的IP地址;
  • 内层目标IP:SLB的公网IP地址。

当隧道在SGW的时候,切换了一个隧道,会从SGW到PGW的隧道,因而网络包的格式为:

  • 外层源MAC:SGW的MAC;
  • 外层目标MAC:PGW的MAC;
  • 外层源IP:SGW的IP;
  • 外层目标IP:PGW的IP;
  • 内层源MAC:手机也即UE的MAC;
  • 内层目标MAC:网关PGW上面的隧道端点的MAC;
  • 内层源IP:UE的IP地址;
  • 内层目标IP:SLB的公网IP地址。

在PGW的隧道端点将包解出来,转发出去的时候,一般在PGW出外部网络的路由器上,会部署NAT服务,将手机的IP地址转换为公网IP地址,当请求返回的时候,再NAT回来。

因而在PGW之后,相当于做了一次欧洲十国游型的转发,网络包的格式为:

  • 源MAC:PGW出口的MAC;
  • 目标MAC:NAT网关的MAC;
  • 源IP:UE的IP地址;
  • 目标IP:SLB的公网IP地址。

在NAT网关,相当于做了一次玄奘西游型的转发,网络包的格式变成:

  • 源MAC:NAT网关的MAC;
  • 目标MAC:A2路由器的MAC;
  • 源IP:UE的公网IP地址;
  • 目标IP:SLB的公网IP地址。

出了NAT网关,就从核心网到达了互联网。在网络世界,每一个运营商的网络成为自治系统AS。每个自治系统都有边界路由器,通过它和外面的世界建立联系。

对于云平台来讲,它可以被称为Multihomed AS,有多个连接连到其他的AS,但是大多拒绝帮其他的AS传输包。例如一些大公司的网络。对于运营商来说,它可以被称为Transit AS,有多个连接连到其他的AS,并且可以帮助其他的AS传输包,比如主干网。

如何从出口的运营商到达云平台的边界路由器?在路由器之间需要通过BGP协议实现,BGP又分为两类,eBGP和iBGP。自治系统之间、边界路由器之间使用eBGP广播路由。内部网络也需要访问其他的自治系统。

边界路由器如何将BGP学习到的路由导入到内部网络呢?通过运行iBGP,使内部的路由器能够找到到达外网目的地最好的边界路由器。

网站的SLB的公网IP地址早已经通过云平台的边界路由器,让全网都知道了。于是这个下单的网络包选择的下一跳是A2,也即将A2的MAC地址放在目标MAC地址中。

到达A2之后,从路由表中找到下一跳是路由器C1,于是将目标MAC换成C1的MAC地址。到达C1之后,找到下一跳是C2,将目标MAC地址设置为C2的MAC。到达C2后,找到下一跳是云平台的边界路由器,于是将目标MAC设置为边界路由器的MAC地址。

你会发现,这一路,都是只换MAC,不换目标IP地址。这就是所谓下一跳的概念。

在云平台的边界路由器,会将下单的包转发进来,经过核心交换,汇聚交换,到达外网网关节点上的SLB的公网IP地址。

我们可以看到,手机到SLB的公网IP,是一个端到端的连接,连接的过程发送了很多包。所有这些包,无论是TCP三次握手,还是HTTPS的密钥交换,都是要走如此复杂的过程到达SLB的,当然每个包走的路径不一定一致。

网络包走在这个复杂的道路上,很可能一不小心就丢了,怎么办?这就需要借助TCP的机制重新发送。

既然TCP要对包进行重传,就需要维护Sequence Number,看哪些包到了,哪些没到,哪些需要重传,传输的速度应该控制到多少,这就是TCP的滑动窗口协议

整个TCP的发送,一开始会协商一个Sequence Number,从这个Sequence Number开始,每个包都有编号。滑动窗口将接收方的网络包分成四个部分:

  • 已经接收,已经ACK,已经交给应用层的包;
  • 已经接收,已经ACK,未发送给应用层;
  • 已经接收,尚未发送ACK;
  • 未接收,尚有空闲的缓存区域。

对于TCP层来讲,每一个包都有ACK。ACK需要从SLB回复到手机端,将上面的那个过程反向来一遍,当然路径不一定一致,可见ACK也不是那么轻松的事情。

如果发送方超过一定的时间没有收到ACK,就会重新发送。只有TCP层ACK过的包,才会发给应用层,并且只会发送一份,对于下单的场景,应用层是HTTP层。

你可能会问了,TCP老是重复发送,会不会导致一个单下了两遍?是否要求服务端实现幂等?从TCP的机制来看,是不会的。只有收不到ACK的包才会重复发,发到接收端,在窗口里面只保存一份,所以在同一个TCP连接中,不用担心重传导致二次下单。

但是TCP连接会因为某种原因断了,例如手机信号不好,这个时候手机把所有的动作重新做一遍,建立一个新的TCP连接,在HTTP层调用两次RESTful API。这个时候可能会导致两遍下单的情况,因而RESTful API需要实现幂等。

当ACK过的包发给应用层之后,TCP层的缓存就空了出来,这会导致上面图中的大三角,也即接收方能够容纳的总缓存,整体顺时针滑动。小的三角形,也即接收方告知发送方的窗口总大小,也即还没有完全确认收到的缓存大小,如果把这些填满了,就不能再发了,因为没确认收到,所以一个都不能扔。

8.从数据中心进网关,公网NAT成私网

包从手机端经历千难万险,终于到了SLB的公网IP所在的公网网口。由于匹配上了MAC地址和IP地址,因而将网络包收了进来。

在虚拟网关节点的外网网口上,会有一个NAT规则,将公网IP地址转换为VPC里面的私网IP地址,这个私网IP地址就是SLB的HAProxy所在的虚拟机的私网IP地址。

当然为了承载比较大的吞吐量,虚拟网关节点会有多个,物理网络会将流量分发到不同的虚拟网关节点。同样HAProxy也会是一个大的集群,虚拟网关会选择某个负载均衡节点,将某个请求分发给它,负载均衡之后是Controller层,也是部署在虚拟机里面的。

当网络包里面的目标IP变成私有IP地址之后,虚拟路由会查找路由规则,将网络包从下方的私网网口发出来。这个时候包的格式为:

  • 源MAC:网关MAC;
  • 目标MAC:HAProxy虚拟机的MAC;
  • 源IP:UE的公网IP;
  • 目标IP:HAProxy虚拟机的私网IP。

9.进入隧道打标签,RPC远程调用下单

在虚拟路由节点上,也会有OVS,将网络包封装在VXLAN隧道里面,VXLAN ID就是给你的租户创建VPC的时候分配的。包的格式为:

  • 外层源MAC:网关物理机MAC;
  • 外层目标MAC:物理机A的MAC;
  • 外层源IP:网关物理机IP;
  • 外层目标IP:物理机A的IP;
  • 内层源MAC:网关MAC;
  • 内层目标MAC:HAProxy虚拟机的MAC;
  • 内层源IP:UE的公网IP;
  • 内层目标IP:HAProxy虚拟机的私网IP。

在物理机A上,OVS会将包从VXLAN隧道里面解出来,发给HAProxy所在的虚拟机。HAProxy所在的虚拟机发现MAC地址匹配,目标IP地址匹配,就根据TCP端口,将包发给HAProxy进程,因为HAProxy是在监听这个TCP端口的。因而HAProxy就是这个TCP连接的服务端,客户端是手机。对于TCP的连接状态、滑动窗口等,都是在HAProxy上维护的。

在这里HAProxy是一个四层负载均衡,也即它只解析到TCP层,里面的HTTP协议它不关心,就将请求转发给后端的多个Controller层的一个。

HAProxy发出去的网络包就认为HAProxy是客户端了,看不到手机端了。网络包格式如下:

  • 源MAC:HAProxy所在虚拟机的MAC;
  • 目标MAC:Controller层所在虚拟机的MAC;
  • 源IP:HAProxy所在虚拟机的私网IP;
  • 目标IP:Controller层所在虚拟机的私网IP。

当然这个包发出去之后,还是会被物理机上的OVS放入VXLAN隧道里面,网络包格式为:

  • 外层源MAC:物理机A的MAC;
  • 外层目标MAC:物理机B的MAC;
  • 外层源IP:物理机A的IP;
  • 外层目标IP:物理机B的IP;
  • 内层源MAC:HAProxy所在虚拟机的MAC;
  • 内层目标MAC:Controller层所在虚拟机的MAC;
  • 内层源IP:HAProxy所在虚拟机的私网IP;
  • 内层目标IP:Controller层所在虚拟机的私网IP。

在物理机B上,OVS会将包从VXLAN隧道里面解出来,发给Controller层所在的虚拟机。Controller层所在的虚拟机发现MAC地址匹配,目标IP地址匹配,就根据TCP端口,将包发给Controller层的进程,因为它在监听这个TCP端口。

在HAProxy和Controller层之间,维护一个TCP的连接。

Controller层收到包之后,它是关心HTTP里面是什么的,于是解开HTTP的包,发现是一个POST请求,内容是下单购买一个课程。

##

10.下单扣减库存优惠券,数据入库返回成功

下单是一个复杂的过程,因而往往在组合服务层会有一个专门管理下单的服务,Controller层会通过RPC调用这个组合服务层。

假设我们使用的是Dubbo,则Controller层需要读取注册中心,将下单服务的进程列表拿出来,选出一个来调用。

Dubbo中默认的RPC协议是Hessian2。Hessian2将下单的远程调用序列化为二进制进行传输。

Netty是一个非阻塞的基于事件的网络传输框架。Controller层和下单服务之间,使用了Netty的网络传输框架。有了Netty,就不用自己编写复杂的异步Socket程序了。Netty使用的方式,就是咱们讲Socket编程的时候,一个项目组支撑多个项目(IO多路复用,从派人盯着到有事通知)这种方式。

Netty还是工作在Socket这一层的,发送的网络包还是基于TCP的。在TCP的下层,还是需要封装上IP头和MAC头。如果跨物理机通信,还是需要封装的外层的VXLAN隧道里面。当然底层的这些封装,Netty都不感知,它只要做好它的异步通信即可。

在Netty的服务端,也即下单服务中,收到请求后,先用Hessian2的格式进行解压缩。然后将请求分发到线程中进行处理,在线程中,会调用下单的业务逻辑。

下单的业务逻辑比较复杂,往往要调用基础服务层里面的库存服务、优惠券服务等,将多个服务调用完毕,才算下单成功。下单服务调用库存服务和优惠券服务,也是通过Dubbo的框架,通过注册中心拿到库存服务和优惠券服务的列表,然后选一个调用。

调用的时候,统一使用Hessian2进行序列化,使用Netty进行传输,底层如果跨物理机,仍然需要通过VXLAN的封装和解封装。

咱们以库存为例子的时候,讲述过幂等的接口实现的问题。因为如果扣减库存,仅仅是谁调用谁减一。这样存在的问题是,如果扣减库存因为一次调用失败,而多次调用,这里指的不是TCP多次重试,而是应用层调用的多次重试,就会存在库存扣减多次的情况。

这里常用的方法是,使用乐观锁(Compare and Set,简称CAS)。CAS要考虑三个方面,当前的库存数、预期原来的库存数和版本,以及新的库存数。在操作之前,查询出原来的库存数和版本,真正扣减库存的时候,判断如果当前库存的值与预期原值和版本相匹配,则将库存值更新为新值,否则不做任何操作。

这是一种基于状态而非基于动作的设计,符合RESTful的架构设计原则。这样的设计有利于高并发场景。当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。

最终,当下单更新到分布式数据库中之后,整个下单过程才算真正告一段落。

好了,经过了十个过程,下单终于成功了,你是否对这个过程了如指掌了呢?如果发现对哪些细节比较模糊,可以回去看一下相应的章节,相信会有更加深入的理解。

有时候我们自己写应用的时候,不一定是直接调用应用层协议的接口,例如HTTP等,而是自己写Socket编程,来约定应用层的协议。再如,ping也是一个应用,但是它没有用传输层的协议,而是用了ICMP的协议。

网络号、IP地址、子网掩码和广播地址的先后关系是什么?

当在一个数据中心或者一个办公室规划一个网络的时候,首先是网络管理员规划网段,一般是根据将来要容纳的机器数量来规划,一旦定了,以后就不好变了。

假如你在一个小公司里,总共就没几台机器,对于私有地址,一般选择192.168.0.0/24就可以了。

这个时候先有的是网络号。192.168.0就是网络号。有了网络号,子网掩码同时也就有了,就是前面都是网络号的是1,其他的是0,广播地址也有了,除了网络号之外都是1。

当规划完网络的时候,一般这个网络里面的第一个、第二个地址被默认网关DHCP服务器占用,你自己创建的机器,只要和其他的不冲突就可以了,当然你也可以让DHCP服务自动配置。

规划网络原来都是网络管理员的事情。有了公有云之后,一般有个概念虚拟网络(VPC),鼠标一点就能创建一个网络,网络完全软件化了,任何人都可以做网络规划。

组播和广播的意义和原理是什么?

类地址的主机号8位,去掉0和255,就只有254个了。

在《TCP/IP详解》这本书里面,有两章讲了广播、多播以及IGMP。广播和组播分为两个层面,其中MAC层有广播和组播对应的地址,IP层也有自己的广播地址和组播地址。

广播相对比较简单,MAC层的广播为ff:ff:ff:ff:ff:ff,IP层指向子网的广播地址为主机号为全1且有特定子网号的地址。

组播复杂一些,MAC层中,当地址中最高字节的最低位设置为1时,表示该地址是一个组播地址,用十六进制可表示为01:00:00:00:00:00。IP层中,组播地址为D类IP地址,当IP地址为组播地址的时候,有一个算法可以计算出对应的MAC层地址。

多播进程将目的IP地址指明为多播地址,设备驱动程序将它转换为相应的以太网地址,然后把数据发送出去。这些接收进程必须通知它们的IP层,它们想接收的发给定多播地址的数据报,并且设备驱动程序必须能够接收这些多播帧。这个过程就是“加入一个多播组”。

当多播跨越路由器的时候,需要通过IGMP协议告诉多播路由器,多播数据包应该如何转发。

MTU 1500的具体含义是什么?

MTU(Maximum Transmission Unit,最大传输单元)是二层的一个定义。以以太网为例,MTU为1500个Byte,前面有6个Byte的目标MAC地址,6个Byte的源MAC地址,2个Byte的类型,后面有4个Byte的CRC校验,共1518个Byte。

在IP层,一个IP数据报在以太网中传输,如果它的长度大于该MTU值,就要进行分片传输。如果不允许分片DF,就会发送ICMP包,这个在ICMP那一节讲过。

在TCP层有个MSS(Maximum Segment Size,最大分段大小),它等于MTU减去IP头,再减去TCP头。即在不分片的情况下,TCP里面放的最大内容。

在HTTP层看来,它的body没有限制,而且在应用层看来,下层的TCP是一个流,可以一直发送,但其实是会被分成一个个段的。

在DHCP网络里面,手动配置IP地址会冲突吗?

在一个DHCP网络里面,如果某一台机器手动配置了一个IP地址,并且在DHCP管理的网段里的话,DHCP服务器是会将这个地址分配给其他机器的。一旦分配了,ARP的时候,就会收到两个应答,IP地址就冲突了。

当发生这种情况的时候,应该怎么办呢?DHCP的过程虽然没有明确如何处理,但是DHCP的客户端和服务器都可以添加相应的机制来检测冲突。

如果由客户端来检测冲突,一般情况是,客户端在接受分配的IP之前,先发送一个ARP,看是否有应答,有就说明冲突了,于是发送一个DHCPDECLINE,放弃这个IP地址。

如果由服务器来检测冲突,DHCP服务器会发送ping,来看某个IP是否已经被使用。如果被使用了,它就不再将这个IP分配给其他的客户端了。

DHCP的Offer和ACK应该是单播还是广播呢?

正常情况下,一旦有了IP地址,DHCP Server还是希望通过单播的方式发送OFFER和ACK。但是不幸的是,有的客户端协议栈的实现,如果还没有配置IP地址,就使用单播。协议栈是不接收这个包的,因为OFFER和ACK的时候,IP地址还没有配置到网卡上。

所以,一切取决于客户端的协议栈的能力,如果没配置好IP,就不能接收单播的包,那就将BROADCAST设为1,以广播的形式进行交互。

如果客户端的协议栈实现很厉害,即便是没有配置好IP,仍然能够接受单播的包,那就将BROADCAST位设置为0,就以单播的形式交互。

DHCP如何解决内网安全问题?

其实DHCP协议的设计是基于内网互信的基础来设计的,而且是基于UDP协议。但是这里面的确是有风险的。例如一个普通用户无意地或者恶意地安装一台DHCP服务器,发放一些错误或者冲突的配置;再如,有恶意的用户发出很多的DHCP请求,让DHCP服务器给他分配大量的IP。

对于第一种情况,DHCP服务器和二层网络都是由网管管理的,可以在交换机配置只有来自某个DHCP服务器的包才是可信的,其他全部丢弃。如果有SDN,或者在云中,非法的DHCP包根本就拦截到虚拟机或者物理机的出口。

对于第二种情况,一方面进行监控,对DHCP报文进行限速,并且异常的端口可以关闭,一方面还是SDN或者在云中,除了被SDN管控端登记过的IP和MAC地址,其他的地址是不允许出现在虚拟机和物理机出口的,也就无法模拟大量的客户端。

STP 协议能够很好地解决环路问题,但是也有它的缺点,你能举几个例子吗?

STP的主要问题在于,当拓扑发生变化,新的配置消息要经过一定的时延才能传播到整个网络。

由于整个交换网络只有一棵生成树,在网络规模比较大的时候会导致较长的收敛时间,拓扑改变的影响面也较大,当链路被阻塞后将不承载任何流量,造成了极大带宽浪费。

每台交换机的武力值是什么样的?

当一台交换机加入或者离开网络的时候,都会造成网络拓扑变化,这个时候检测到拓扑变化的网桥会通知根网桥,根网桥会通知所有的网桥拓扑发生变化。

网桥的ID是由网桥优先级和网桥MAC地址组成的,网桥ID最小的将成为网络中的根桥。默认配置下,网桥优先级都一样,默认优先级是32768。这个时候MAC地址最小的网桥成为根网桥。但是如果你想设置某台为根网桥,就配置更小的优先级即可。

在优先级向量里面,Root Bridge ID就是根网桥的ID,Bridge ID是网桥的ID,Port ID就是一个网桥上有多个端口,端口的ID。

在MAC地址已经学习的情况下,ARP会广播到没有IP的物理段吗?

这里ARP的目标地址是广播的,所以无论是否进行地址学习,都会广播,而对于某个MAC的访问,在没有地址学习的时候,是转发到所有的端口的,学习之后,只会转发到有这个MAC的端口。

802.1Q VLAN 和Port-based VLAN有什么区别?

所谓Port-based VLAN,一般只在一台交换机上起作用,比如一台交换机,10个口,1、3、5、7、9属于VLAN 10。1发出的包,只有3、5、7、9能够收到,但是从这些口转发出去的包头中,并不带VLAN ID。

而802.1Q的VLAN,出了交换机也起作用,也就是说,一旦打上某个VLAN,则出去的包都带这个VLAN,也需要链路上的交换机能够识别这个VLAN,进行转发。

当发送的报文出问题的时候,会发送一个ICMP的差错报文来报告错误,但是如果 ICMP 的差错报文也出问题了呢?

我总结了一下,不会导致产生ICMP差错报文的有:

  • ICMP差错报文(ICMP查询报文可能会产生ICMP差错报文);
  • 目的地址是广播地址或多播地址的IP数据报;
  • 作为链路层广播的数据报;
  • 不是IP分片的第一片;
  • 源地址不是单个主机的数据报。这就是说,源地址不能为零地址、环回地址、广播地址或多播地址。

ping使用的是什么网络编程接口?

咱们使用的网络编程接口是Socket,对于ping来讲,使用的是ICMP,创建Socket如下:

1
socket(AF_INET, SOCK_RAW, IPPROTO_ICMP)

SOCK_RAW就是基于IP层协议建立通信机制。

如果是TCP,则建立下面的Socket:

1
socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)

如果是UDP,则建立下面的Socket:

1
socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP)

ICMP差错报文是谁发送的呢?

我看留言里有很多人对这个问题有疑惑。ICMP包是由内核返回的,在内核中,有一个函数用于发送ICMP的包。

1
void icmp_send(struct sk_buff *skb_in, int type, int code, __be32 info);

例如,目标不可达,会调用下面的函数。

1
icmp_send(skb, ICMP_DEST_UNREACH, ICMP_PROT_UNREACH, 0);

当IP大小超过MTU的时候,发送需要分片的ICMP。

1
2
3
4
if (ip_exceeds_mtu(skb, mtu)) {
icmp_send(skb, ICMP_DEST_UNREACH, ICMP_FRAG_NEEDED, htonl(mtu));
goto drop;
}

当在你家里要访问 163 网站的时候,你的包需要 NAT 成为公网 IP,返回的包又要 NAT 成你的私有 IP,返回包怎么知道这是你的请求呢?它怎么能这么智能的 NAT 成了你的 IP 而非别人的 IP 呢?

这是个比较复杂的事情。在讲云中网络安全里的iptables时,我们讲过conntrack功能,它记录了SNAT一去一回的对应关系。

如果编译内核时开启了连接跟踪选项,那么Linux系统就会为它收到的每个数据包维持一个连接状态,用于记录这条数据连接的状态。

根据咱们学过的Netfilter的流程图,我们知道,网络包有三种路径:

  • 发给我的,从PREROUTING到INPUT,我就接收了;
  • 我发给别人的,从OUTPUT到POSTROUTING,就发出去的;
  • 从我这里经过的,从PREROUTING到FORWARD到POSTROUTING。

如果要跟踪一个网络包,对于每一种路径,都需要设置两个记录点,相当于打两次卡,这样内核才知道这个包的状态。

对于这三种路径,打卡的点是这样设置的:

  • 发给我的,在PREROUTING调用ipv4_conntrack_in,创建连接跟踪记录;在INPUT调用ipv4_confirm,将这个连接跟踪记录挂在内核的连接跟踪表里面。为什么不一开始就挂在内核的连接跟踪表里面呢?因为有filter表,一旦把包过滤了,也就是丢弃了,那根本没必要记录这个连接了。
  • 我发给别人的,在OUTPUT调用ipv4_conntrack_local,创建连接跟踪记录,在POSTROUTING调用ipv4_confirm,将这个连接跟踪记录挂在内核的连接跟踪表里面。
  • 从我这里经过的,在PREROUTING调用ipv4_conntrack_in,创建连接跟踪记录,在POSTROUTING调用ipv4_confirm,将这个连接跟踪记录挂在内核的连接跟踪表里面。

网关主要做转发,这里主要说的是NAT网关,因而我们重点来看“从我这里经过的”这种场景,再加上要NAT,因而将NAT的过程融入到连接跟踪的过程中来:

  • 如果是PREROUTING的时候,先调用ipv4_conntrack_in,创建连接跟踪记录;
  • 如果是PREROUTING的时候,有NAT规则,则调用nf_nat_ipv4_in进行地址转换;
  • 如果是POSTROUTING的时候,有NAT规则,则调用nf_nat_ipv4_out进行地址转换;
  • 如果是POSTROUTING的时候,调用ipv4_confirm,将这个连接跟踪记录挂在内核的连接跟踪表里面。

接下来,我们来看,在这个过程中涉及到的数据结构:连接跟踪记录、连接跟踪表。

在前面讲网络包处理的时候,我们说过,每个网络包都是一个struct sk_buff,它有一个成员变量_nfct指向一个连接跟踪记录struct nf_conn。当然当一个网络包刚刚进来的时候,是不会指向这么一个结构的,但是这个网络包肯定属于某个连接,因而会去连接跟踪表里面去查找,之后赋值给sk_buff的这个成员变量。没找到的话,就说明是一个新的连接,然后会重新创建一个。

连接跟踪记录里面有几个重要的东西:

  • nf_conntrack其实才是_nfct变量指向的地址,但是没有关系,学过C++的话应该明白,对于结构体来讲,nf_conn和nf_conntrack的起始地址是一样的;
  • tuplehash虽然是数组,但是里面只有两个,IP_CT_DIR_ORIGINAL为下标0,表示连接的发起方向,IP_CT_DIR_REPLY为下标1,表示连接的回复方向。
1
2
3
4
5
6
7
8
9
struct nf_conn {
......
struct nf_conntrack ct_general;
......
struct nf_conntrack_tuple_hash tuplehash[IP_CT_DIR_MAX];
......
unsigned long status;
......
}

在这里面,最重要的是nf_conntrack_tuple_hash的数组。nf_conn是这个网络包对应的一去一回的连接追踪记录,但是这个记录是会放在一个统一的连接追踪表里面的。

连接跟踪表nf_conntrack_hash是一个数组,数组中的每一项都是一个双向链表的头,每一项后面都挂着一个双向链表,链表中的每一项都是这个结构。

这个结构的第一项是链表的链,nf_conntrack_tuple是用来标识是否同一个连接。

从上面可以看出来,连接跟踪表是一个典型的链式哈希表的实现。

每当有一个网络包来了的时候,会将网络包中sk_buff中的数据提取出来,形成nf_conntrack_tuple,并根据里面的内容计算哈希值。然后需要在哈希表中查找,如果找到,则说明这个连接出现过;如果没找到,则生成一个插入哈希表。

通过nf_conntrack_tuple里面的内容,可以唯一地标识一个连接:

  • src:包含源IP地址;如果是TCP或者UDP,包含源端口;如果是ICMP,包含的是ID;
  • dst:包含目标IP地址;如果是TCP或者UDP,包含目标端口;如果是ICMP,包含的是type, code。

有了这些数据结构,我们接下来看这一去一回的过程。

当一个包发出去的时候,到达这个NAT网关的时候,首先经过PREROUTING的时候,先调用ipv4_conntrack_in。这个时候进来的包sk_buff为: {源IP:客户端IP,源端口:客户端port,目标IP:服务端IP,目标端口:服务端port},将这个转换为nf_conntrack_tuple,然后经过哈希运算,在连接跟踪表里面查找,发现没有,说明这是一个新的连接。

于是,创建一个新的连接跟踪记录nf_conn,这里面有两个nf_conntrack_tuple_hash:

  • 一去:{源IP:客户端IP,源端口:客户端port,目标IP:服务端IP,目标端口:服务端port};
  • 一回:{源IP:服务端IP,源端口:服务端port,目标IP:客户端IP,目标端口:客户端port}。

接下来经过FORWARD过程,假设包没有被filter掉,于是要转发出去,进入POSTROUTING的过程,有NAT规则,则调用nf_nat_ipv4_out进行地址转换。这个时候,源地址要变成NAT网关的IP地址,对于masquerade来讲,会自动选择一个公网IP地址和一个随机端口。

为了让包回来的时候,能找到连接跟踪记录,需要修改两个nf_conntrack_tuple_hash中回来的那一项为:{源IP:服务端IP,源端口:服务端port,目标IP:NAT网关IP,目标端口:随机端口}。

接下来要将网络包真正发出去的时候,除了要修改包里面的源IP和源端口之外,还需要将刚才的一去一回的两个nf_conntrack_tuple_hash放入连接跟踪表这个哈希表中。

当网络包到达服务端,然后回复一个包的时候,这个包sk_buff为:{源IP:服务端IP,源端口:服务端port,目标IP:NAT网关IP,目标端口:随机端口}。

将这个转换为nf_conntrack_tuple后,进行哈希运算,在连接跟踪表里面查找,是能找到相应的记录的,找到nf_conntrack_tuple_hash之后,Linux会提供一个函数。

1
2
3
4
5
6
static inline struct nf_conn *
nf_ct_tuplehash_to_ctrack(const struct nf_conntrack_tuple_hash *hash)
{
return container_of(hash, struct nf_conn,
tuplehash[hash->tuple.dst.dir]);
}

可以通过nf_conntrack_tuple_hash找到外面的连接跟踪记录nf_conn,通过这个可以找到来方向的那个nf_conntrack_tuple_hash,{源IP:客户端IP,源端口:客户端port,目标IP:服务端IP,目标端口:服务端port},这样就能够找到客户端的IP和端口,从而可以NAT回去。

NAT能建立多少连接?

SNAT多用于内网访问外网的场景,鉴于conntrack是由{源IP,源端口,目标IP,目标端口},hash后确定的。

如果内网机器很多,但是访问的是不同的外网,也即目标IP和目标端口很多,这样内网可承载的数量就非常大,可不止65535个。

但是如果内网所有的机器,都一定要访问同一个目标IP和目标端口,这样源IP如果只有一个,这样的情况下,才受65535的端口数目限制,根据原理,一种方法就是多个源IP,另外的方法就是多个NAT网关,来分摊不同的内网机器访问。

如果你使用的是公有云,65535台机器,应该放在一个VPC里面,可以放在多个VPC里面,每个VPC都可以有自己的NAT网关。

其实SNAT的场景是内网访问外网,存在端口数量的问题,也是所有的机器都访问一个目标地址的情况。

如果是微信这种场景,应该是服务端在数据中心内部,无论多少长连接,作为服务端监听的都是少数几个端口,是DNAT的场景,是没有端口数目问题的,只有一台服务器能不能维护这么多连接,因而在NAT网关后面部署多个nginx来分摊连接即可。

公网IP和私网IP需要一一绑定吗?

公网IP是有限的,如果使用公有云,需要花钱去买。但是不是每一个虚拟机都要有一个公网IP的,只有需要对外提供服务的机器,也即接入层的那些nginx需要公网IP,没有公网IP,使用SNAT,大家共享SNAT网关的公网IP地址,也是能够访问的外网的。

我看留言中的困惑点都在于,要区分内主动发起访问外,还是外主动发起访问内,是访问同一个服务端,还是访问一大批服务端。这里就很明白了。

路由协议要在路由器之间交换信息,这些信息的交换还需要走路由吗?不是死锁了吗?

OSPF是直接基于IP协议发送的,而且OSPF的包都是发给邻居的,也即只有一跳,不会中间经过路由设备。BGP是基于TCP协议的,在BGP peer之间交换信息。

多线BGP机房是怎么回事儿?

BGP主要用于互联网AS自治系统之间的互联,BGP的最主要功能在于控制路由的传播选择最好的路由。各大运营商都具有AS号,全国各大网络运营商多数都是通过BGP协议与自身的AS来实现多线互联的。

使用此方案来实现多线路互联,IDC需要在CNNIC(中国互联网信息中心)或APNIC(亚太网络信息中心)申请自己的IP地址段和AS号,然后通过BGP协议将此段IP地址广播到其它的网络运营商的网络中。

使用BGP协议互联后,网络运营商的所有骨干路由设备将会判断到IDC机房IP段的最佳路由,以保证不同网络运营商用户的高速访问。

TIME_WAIT状态太多是怎么回事儿?

如果处于TIMEWAIT状态,说明双方建立成功过连接,而且已经发送了最后的ACK之后,才会处于这个状态,而且是主动发起关闭的一方处于这个状态。

如果存在大量的TIMEWAIT,往往是因为短连接太多,不断的创建连接,然后释放连接,从而导致很多连接在这个状态,可能会导致无法发起新的连接。解决的方式往往是:

  • 打开tcp_tw_recycle和tcp_timestamps选项;
  • 打开tcp_tw_reuse和tcp_timestamps选项;
  • 程序中使用SO_LINGER,应用强制使用rst关闭。

当客户端收到Connection Reset,往往是收到了TCP的RST消息,RST消息一般在下面的情况下发送:

  • 试图连接一个未被监听的服务端;
  • 对方处于TIMEWAIT状态,或者连接已经关闭处于CLOSED状态,或者重新监听seq num不匹配;
  • 发起连接时超时,重传超时,keepalive超时;
  • 在程序中使用SO_LINGER,关闭连接时,放弃缓存中的数据,给对方发送RST。

起始序列号是怎么计算的,会冲突吗?

TCP初始化序列号不能设置为一个固定值,因为这样容易被攻击者猜出后续序列号,从而遭到攻击。 RFC1948中提出了一个较好的初始化序列号ISN随机生成算法。

ISN = M + F (localhost, localport, remotehost, remoteport)

M是一个计时器,这个计时器每隔4毫秒加1。F是一个Hash算法,根据源IP、目的IP、源端口、目的端口生成一个随机数值。要保证Hash算法不能被外部轻易推算得出,用MD5算法是一个比较好的选择。

epoll是Linux上的函数,那你知道Windows上对应的机制是什么吗?如果想实现一个跨平台的程序,你知道应该怎么办吗?

epoll是异步通知,当事件发生的时候,通知应用去调用IO函数获取数据。IOCP异步传输,当事件发生时,IOCP机制会将数据直接拷贝到缓冲区里,应用可以直接使用。

如果跨平台,推荐使用libevent库,它是一个事件通知库,适用于Windows、Linux、BSD等多种平台,内部使用select、epoll、kqueue、IOCP等系统调用管理事件机制。

QUIC是一个精巧的协议,所以它肯定不止今天我提到的四种机制,你知道还有哪些吗?

QUIC还有其他特性,一个是快速建立连接。这个我放在下面HTTPS的时候一起说。另一个是拥塞控制,QUIC协议当前默认使用了TCP协议的CUBIC(拥塞控制算法)。

你还记得TCP的拥塞控制算法吗?每当收到一个ACK的时候,就需要调整拥塞窗口的大小。但是这也造成了一个后果,那就是RTT比较小的,窗口增长快。

然而这并不符合当前网络的真实状况,因为当前的网络带宽比较大,但是由于遍布全球,RTT也比较长,因而基于RTT的窗口调整策略,不仅不公平,而且由于窗口增加慢,有时候带宽没满,数据就发送完了,因而巨大的带宽都浪费掉了。

CUBIC进行了不同的设计,它的窗口增长函数仅仅取决于连续两次拥塞事件的时间间隔值,窗口增长完全独立于网络的时延RTT。

CUBIC的窗口大小以及变化过程如图所示。

当出现丢包事件时,CUBIC会记录这时的拥塞窗口大小,把它作为Wmax。接着,CUBIC会通过某个因子执行拥塞窗口的乘法减小,然后,沿着立方函数进行窗口的恢复。

从图中可以看出,一开始恢复的速度是比较快的,后来便从快速恢复阶段进入拥塞避免阶段,也即当窗口接近Wmax的时候,增加速度变慢;立方函数在Wmax处达到稳定点,增长速度为零,之后,在平稳期慢慢增长,沿着立方函数的开始探索新的最大窗口。

HTTP的keepalive模式是什么样?

在没有keepalive模式下,每个HTTP请求都要建立一个TCP连接,并且使用一次之后就断开这个TCP连接。

使用keepalive之后,在一次TCP连接中可以持续发送多份数据而不会断开连接,可以减少TCP连接建立次数,减少TIME_WAIT状态连接。

然而,长时间的TCP连接容易导致系统资源无效占用,因而需要设置正确的keepalive timeout时间。当一个HTTP产生的TCP连接在传送完最后一个响应后,还需要等待keepalive timeout秒后,才能关闭这个连接。如果这个期间又有新的请求过来,可以复用TCP连接。

HTTPS 协议比较复杂,沟通过程太繁复,这样会导致效率问题,那你知道有哪些手段可以解决这些问题吗?

通过HTTPS访问的确复杂,至少经历四个阶段:DNS查询、TCP连接建立、TLS连接建立,最后才是HTTP发送数据。我们可以一项一项来优化这个过程。

首先如果使用基于UDP的QUIC,可以省略掉TCP的三次握手。至于TLS的建立,如果按文章中基于TLS 1.2的,双方要交换key,经过两个来回,也即两个RTT,才能完成握手。但是咱们讲IPSec的时候,讲过通过共享密钥、DH算法进行握手的场景。

在TLS 1.3中,握手过程中移除了ServerKeyExchange和ClientKeyExchange,DH参数可以通过key_share进行传输。这样只要一个来回,就可以搞定RTT了。

对于QUIC来讲,也可以这样做。当客户端首次发起QUIC连接时,会发送一个client hello消息,服务器会回复一个消息,里面包括server config,类似于TLS1.3中的key_share交换。当客户端获取到server config以后,就可以直接计算出密钥,发送应用数据了。

如果权威DNS连不上,怎么办?

一般情况下,DNS是基于UDP协议的。在应用层设置一个超时器,如果UDP发出没有回应,则会进行重试。

DNS服务器一般也是高可用的,很少情况下会挂。即便挂了,也会很快切换,重试一般就会成功。

对于客户端来讲,为了DNS解析能够成功,也会配置多个DNS服务器,当一个不成功的时候,可以选择另一个来尝试。

对于数据中心来讲,高可用是非常重要的,每个设备都要考虑高可用,那跨机房的高可用,你知道应该怎么做吗?

其实跨机房的高可用分两个级别,分别是同城双活异地灾备

同城双活,就是在同一个城市,距离大概30km到100km的两个数据中心之间,通过高速专线互联的方式,让两个数据中心形成一个大二层网络。

同城双活最重要的是,数据如何从一个数据中心同步到另一个数据中心,并且在一个数据中心故障的时候,实现存储设备的切换,保证状态能够快速切换到另一个数据中心。在高速光纤互联情况下,主流的存储厂商都可以做到在一定距离之内的两台存储设备的近实时同步。数据双活是一切双活的基础。

基于双数据中心的数据同步,可以形成一个统一的存储池,从而数据库层在共享存储池的情况下可以近实时的切换,例如Oracle RAC。

虚拟机在统一的存储池的情况下,也可以实现跨机房的HA,在一个机房切换到另一个机房。

SLB负载均衡实现同一机房的各个虚拟机之间的负载均衡。GSLB可以实现跨机房的负载均衡,实现外部访问的切换。

如果在两个数据中心距离很近,并且大二层可通的情况下,也可以使用VRRP协议,通过VIP方式进行外部访问的切换。

下面我们说异地灾备

异地灾备的第一大问题还是数据的问题,也即生产数据中心的数据如何备份到容灾数据中心。由于异地距离比较远,不可能像双活一样采取近同步的方式,只能通过异步的方式进行同步。可以预见的问题是,容灾切换的时候,数据会丢失一部分。

由于容灾数据中心平时是不用的,不是所有的业务都会进行容灾,否则成本太高。

对于数据的问题,我比较建议从业务层面进行容灾。由于数据同步会比较慢,可以根据业务需求高优先级同步重要的数据,因而容灾的层次越高越好。

例如,有的用户完全不想操心,直接使用存储层面的异步复制。对于存储设备来讲,它是无法区分放在存储上的虚拟机,哪台是重要的,哪台是不重要的,只会完全根据块进行复制,很可能就会先复制了不重要的虚拟机。

如果用户想对虚拟机做区分,则可以使用虚拟机层面的异步复制。用户知道哪些虚拟机更重要一些,哪些虚拟机不重要,则可以先同步重要的虚拟机。

对业务来讲,如果用户可以根据业务层情况,在更细的粒度上区分数据是否重要。重要的数据,例如交易数据,需要优先同步;不重要的数据,例如日志数据,就不需要优先同步。

在有异地容灾的情况下,可以平时进行容灾演练,看容灾数据中心是否能够真正起作用,别容灾了半天,最后用的时候掉链子。

DH算法会因为传输随机数被破解吗?

DH算法的交换材料要分公钥部分和私钥部分,公钥部分和其他非对称加密一样,都是可以传输的,所以对于安全性是没有影响的,而且传输材料远比传输原始的公钥更加安全。私钥部分是谁都不能给的,因此也是不会截获到的。

咱们上网都有套餐,有交钱多的,有交钱少的,你知道移动网络是如何控制不同优先级的用户的上网流量的吗?

这个其实是PCRF协议进行控制的,它可以下发命令给PGW来控制上网的行为和特性。

SDN控制器是什么东西?

SDN控制器是一个独立的集群,主要是在管控面,因为要实现一定的高可用性。

主流的开源控制器有OpenContrail、OpenDaylight等。当然每个网络硬件厂商都有自己的控制器,而且可以实现自己的私有协议,进行更加细粒度的控制,所以江湖一直没有办法统一。

流表是在每一台宿主机上保存的,大小限制取决于内存,而集中存放的缺点就是下发会很慢。

这一节中重点讲了iptables的filter和nat功能,iptables还可以通过QUEUE实现负载均衡,你知道怎么做吗?

我们可以在iptables里面添加下面的规则:

1
2
-A PREROUTING -p tcp -m set --match-set minuteman dst,dst -m tcp --tcp-flags FIN,SYN,RST,ACK SYN -j NFQUEUE --queue-balance 50:58
-A OUTPUT -p tcp -m set --match-set minuteman dst,dst -m tcp --tcp-flags FIN,SYN,RST,ACK SYN -j NFQUEUE --queue-balance 50:58

NFQUEUE的规则表示将把包的处理权交给用户态的一个进程。–queue-balance表示会将包发给几个queue。

libnetfilter_queue是一个用户态库,用户态进程会使用libnetfilter_queue连接到这些queue中,将包读出来,根据包的内容做决策后,再放回内核进行发送。

这一节中提到,入口流量其实没有办法控制,出口流量是可以很好控制的,你能想出一个控制云中的虚拟机的入口流量的方式吗?

在云平台中,我们可以限制一个租户的默认带宽,我们仍然可以配置点对点的流量控制。

在发送方的OVS上,我们可以统计发送方虚拟机的网络统计数据,上报给管理面。在接收方的OVS上,我们同样可以收集接收方虚拟机的网络统计数据,上报给管理面。

当流量过大的时候,我们虽然不能控制接收方的入口流量,但是我们可以在管理面下发一个策略,控制发送方的出口流量。

虽然VXLAN可以支持组播,但是如果虚拟机数目比较多,在Overlay网络里面,广播风暴问题依然会很严重,你能想到什么办法解决这个问题吗?

很多情况下,物理机可以提前知道对端虚拟机的MAC地址,因而当发起ARP请求的时候,不用广播全网,只要本地返回就可以了,在Openstack里面称为L2Population。

通过 Flannel 的网络模型可以实现容器与容器直接跨主机的互相访问,那你知道如果容器内部访问外部的服务应该怎么融合到这个网络模型中吗?

Pod内到外部网络是通过docker引擎在iptables的POSTROUTING中的MASQUERADE规则实现的,将容器的地址伪装为node IP出去,回来时再把包nat回容器的地址。

有的时候,我们想给外部的一个服务使用一个固定的域名,这就需要用到Kubernetes里headless service的ExternalName。我们可以将某个外部的地址赋给一个Service的名称,当容器内访问这个名字的时候,就会访问一个虚拟的IP。然后,在容器所在的节点上,由iptables规则映射到外部的IP地址。

在讲述Service Mesh的时候,我们说了,希望Envoy能够在服务不感知的情况下,将服务之间的调用全部代理了,你知道怎么做到这一点吗?

首先,定义的一条规则是ISTIO_REDIRECT转发链。这条链不管三七二十一,都将网络包转发给envoy的15000端口。但是一开始这条链没有被挂到iptables默认的几条链中,所以不起作用。

接下来,在PREROUTING规则中使用这个转发链。从而,进入容器的所有流量都被先转发到envoy的15000端口。而envoy作为一个代理,已经被配置好了,将请求转发给productpage程序。

当productpage往后端进行调用的时候,就碰到了output链。这个链会使用转发链,将所有出容器的请求都转发到envoy的15000端口。

这样,无论是入口的流量,还是出口的流量,全部用envoy做成了“汉堡包”。envoy根据服务发现的配置,做最终的对外调用。

这个时候,iptables规则会对从envoy出去的流量做一个特殊处理,允许它发出去,不再使用上面的output规则。