kk Blog —— 通用基础


date [-d @int|str] [+%s|"+%F %T"]
netstat -ltunp
sar -n DEV 1

ssl SNI(Server Name Indication)

https://blog.csdn.net/makenothing/article/details/53292335

Server Name Indication(SNI)

SNI (Server Name Indication)是用来改善服务器与客户端 SSL (Secure Socket Layer)和 TLS (Transport Layer Security) 的一个扩展。主要解决一台服务器只能使用一个证书(一个域名)的缺点,随着服务器对虚拟主机的支持,一个服务器上可以为多个域名提供服务,因此SNI必须得到支持才能满足需求。

SNI产生背景

SSL以及TLS(SSL的升级版)为客户端与服务器端进行安全连接提供了条件。但是,由于当时技术限制,SSL初期的设计顺应经典的公钥基础设施 PKI(Public Key Infrastructure)设计,PKI 认为一个服务器只为一个域名提供服务,从而一个服务器上也就只能使用一个证书。这样客户端在发送请求的时候,利用DNS域名解析,只要向解析到的IP地址(服务器地址)发送请求,然后服务器将自身唯一的证书返回回来,交给客户端验证,验证通过,则继续进行后续通信。然后通过协商好的加密通道,获得所需要的内容。这意味着服务器可以在 SSL 的启动动阶段发送或提交证书,因为它知道它在为哪个特定的域名服务。

随着HTTP 服务器开启虚拟主机支持后,每个服务器通过相同的IP地址可以为很多域名提供服务。这种为虚拟主机提供通信安全的简单途径,却经常导致使用了错误的数字证书,因为服务器端无法知道客户端到底请求的是哪个域名下的服务,从而导致浏览器对用户发出警告。

不幸的是,当设置了 SSL加密,服务器在读取HTTP请求里面的域名之前已经向客户端提交了证书,也就是已经为默认域提供了服务。但是,一个服务器可能为上千个域名提供服务,不可能将所有证书都发送给客户端,让客户端一一验证,找到与请求域名对应的证书。SNI的设计目的是为了让服务器根据请求来决定为哪个域服务,这个信息通常从HTTP请求头获得。

SSL/TLS握手

熟悉SSL/TLS握手过程的都知道,主要经过以下几个过程: 基于RSA握手和密钥交换的客户端验证服务器为示例详解TLS/SSL握手过程。

1 C->S:client_hello

客户端发起请求,以明文传输请求信息,包含版本信息,加密套件候选列表,压缩算法候选列表,随机数,扩展字段等信息。

SSL/STL版本支持的最高TSL协议版本version,从低到高依次 SSLv2 SSLv3 TLSv1 TLSv1.1 TLSv1.2,当前基本不再使用低于 TLSv1 的版本;

客户端支持的加密套件 cipher suites 列表, 每个加密套件对应前面 TLS 原理中的四个功能的组合:认证算法 Au (身份验证)、密钥交换算法 KeyExchange(密钥协商)、对称加密算法 Enc (信息加密)和信息摘要 Mac(完整性校验);

支持的压缩算法 compression methods 列表,用于后续的信息压缩传输;

随机数 random_C,用于后续的密钥的生成;

扩展字段 extensions,支持协议与算法的相关参数以及其它辅助信息等,常见的 SNI 就属于扩展字段,后续单独讨论该字段作用。

2 server_hello+server_certificate+sever_hello_done

server_hello, 服务端返回协商的信息结果,包括选择使用的协议版本 version,选择的加密套件 cipher suite,选择的压缩算法 compression method、随机数 random_S 等,其中随机数用于后续的密钥协商;

server_certificates, 服务器端配置对应的证书链,用于身份验证与密钥交换;

server_hello_done,通知客户端 server_hello 信息发送结束;

3 证书校验

客户端验证证书的合法性,如果验证通过才会进行后续通信,否则根据错误情况不同做出提示和操作,合法性验证包括如下:

证书链的可信性 trusted certificate path,方法如前文所述;

证书是否吊销 revocation,有两类方式离线 CRL 与在线 OCSP,不同的客户端行为会不同;

有效期 expiry date,证书是否在有效时间范围;

域名 domain,核查证书域名是否与当前的访问域名匹配,匹配规则后续分析;

4 client_key_exchange+change_cipher_spec+encrypted_handshake_message

client_key_exchange,合法性验证通过之后,客户端计算产生随机数字 Pre-master,并用证书公钥加密,发送给服务器;

此时客户端已经获取全部的计算协商密钥需要的信息:两个明文随机数 random_C 和 random_S 与自己计算产生的 Pre-master,计算得到协商密钥;

enc_key=Fuc(random_C, random_S, Pre-Master)

change_cipher_spec,客户端通知服务器后续的通信都采用协商的通信密钥和加密算法进行加密通信;

encrypted_handshake_message,结合之前所有通信参数的 hash 值与其它相关信息生成一段数据,采用协商密钥 session secret 与算法进行加密,然后发送给服务器用于数据与握手验证;

5 change_cipher_spec+encrypted_handshake_message

服务器用私钥解密加密的 Pre-master 数据,基于之前交换的两个明文随机数 random_C 和 random_S,计算得到协商密钥:enc_key=Fuc(random_C, random_S, Pre-Master);

计算之前所有接收信息的 hash 值,然后解密客户端发送的 encrypted_handshake_message,验证数据和密钥正确性;

change_cipher_spec, 验证通过之后,服务器同样发送 change_cipher_spec 以告知客户端后续的通信都采用协商的密钥与算法进行加密通信;

encrypted_handshake_message, 服务器也结合所有当前的通信参数信息生成一段数据并采用协商密钥 session secret 与算法加密并发送到客户端;

6 握手结束

客户端计算所有接收信息的 hash 值,并采用协商密钥解密 encrypted_handshake_message,验证服务器发送的数据和密钥,验证通过则握手完成;

7 加密通信

开始使用协商密钥与算法进行加密通信。

由以上过程可以知道,没有SNI的情况下,服务器无法预知客户端到底请求的是哪一个域名的服务。

SNI 应用

SNI的TLS扩展通过发送虚拟域的名字做为TSL协商的一部分修正了这个问题,在Client Hello阶段,通过SNI扩展,将域名信息提前告诉服务器,服务器根据域名取得对应的证书返回给客户端已完成校验过程。

curl

Linux中主要的网络交互工具,curl 7.18.1+ & openssl 0.9.8j+ 可以支持SNI,CentOS6.5及以下都是curl 7.15 不支持SNI,curl 7.21.3 又支持了–resolve 参数,可以直接定位到IP地址进行访问,对于一个域名有多个部署节点的服务来说,这个参数可以定向的访问某个设备。基本语法为:

1
2
Example:
   curl -k -I --resolve www.example.com:80:192.0.2.1 https://www.example.com/index.html

WireShark抓包验证SNI

使用curl7.15 (不支持SNI)抓包结果:

使用curl7.43(支持SNI)抓包结果:

可以看到,使用curl7.15抓包得到的数据无SNI扩展,而是用curl7.43抓包得到的数据,包含SNI扩展,其中包含host信息。

SSL协议握手过程报文解析

https://blog.csdn.net/tterminator/article/details/50675540

SSL建立握手连接目的:

1.身份的验证,client与server确认对方是它相连接的,而不是第三方冒充的,通过证书实现

2.client与server交换session key,用于连接后数据的传输加密和hash校验

简单的SSL握手连接过程(仅Server端交换证书给client):

1.client发送ClientHello,指定版本,随机数(RN),所有支持的密码套件(CipherSuites)

2.server回应ServerHello,指定版本,RN,选择CipherSuites,会话ID(Session ID)

3.server发送Certificate

4.Server发送ServerHelloDone

5.Client发送ClientKeyExchange,用于与server交换session key

6.Client发送ChangeCipherSpec,指示Server从现在开始发送的消息都是加密过的

7.Client发送Finishd,包含了前面所有握手消息的hash,可以让server验证握手过程是否被第三方篡改

8.Server发送ChangeCipherSpec,指示Client从现在开始发送的消息都是加密过的

9.Server发送Finishd,包含了前面所有握手消息的hash,可以让client验证握手过程是否被第三方篡改,并且证明自己是Certificate密钥的拥有者,即证明自己的身份

下面从抓包数据来具体分析这一过程并说明各部分数据的作用以及如实现前面列出的握手的目标,当然了,最重要的还是说明为何这一过程是安全可靠的,第三方无法截获,篡改或者假冒

1.client发送ClientHello

每一条消息都会包含有ContentType,Version,HandshakeType等信息。

ContentType指示SSL通信处于哪个阶段,是握手(Handshake),开始加密传输(ChangeCipherSpec)还是正常通信(Application)等,见下表

1
2
3
4
5
6
Hex  Dec Type

0x14  20  ChangeCipherSpec
0x15  21  Alert
0x16  22  Handshake
0x17  23  Application

Version是TLS的版本,见下表

1
2
3
4
5
6
Major Version    Minor Version   Version Type

3 0   SSLv3
3 1   TLS 1.0
3 2   TLS 1.1
3 3   TLS 1.2

Handshake Type是在handshanke阶段中的具体哪一步,见下表

1
2
3
4
5
6
7
8
9
10
11
12
Code Description

0 HelloRequest
1 ClientHello
2 ServerHello
11    Certificate
12    ServerKeyExchange
13    CertificateRequest
14    ServerHelloDone
15    CertificateVerify
16    ClientKeyExchange
20    Finished

ClientHello附带的数据随机数据RN,会在生成session key时使用,Cipher suite列出了client支持的所有加密算法组合,可以看出每一组包含3种算法,一个是非对称算法,如RSA,一个是对称算法如DES,3DES,RC4等,一个是Hash算法,如MD5,SHA等,server会从这些算法组合中选取一组,作为本次SSL连接中使用。

2.server回应ServerHello

ession id,如果SSL连接断开,再次连接时,可以使用该属性重新建立连接,在双方都有缓存的情况下可以省略握手的步骤。

server端也会生成随机的RN,用于生成session key使用

server会从client发送的Cipher suite列表中跳出一个,这里挑选的是RSA+RC4+MD5

这次server共发送的3个handshake 消息:Serverhello,Certificate和ServerHelloDone,共用一个ContentType:Handshake

3.server发送Certificate

server的证书信息,只包含public key,server将该public key对应的private key保存好,用于证明server是该证书的实际拥有者,那么如何验证呢?原理很简单:client随机生成一串数,用server这里的public key加密(显然是RSA算法),发给server,server用private key解密后返回给client,client与原文比较,如果一致,则说明server拥有private key,也就说明与client通信的正是证书的拥有者,因为public key加密的数据,只有private key才能解密,目前的技术还没发破解。利用这个原理,也能实现session key的交换,加密前的那串随机数就可用作session key,因为除了client和server,没有第三方能获得该数据了。原理很简单,实际使用时会复杂很多,数据经过多次hash,伪随机等的运算,前面提到的client和server端得RN都会参与计算。

4.Server发送ServerHelloDone

5.Client发送ClientKeyExchange

client拿到server的certificate后,就可以开始利用certificate里的public key进行session key的交换了。从图中可以看出,client发送的是130字节的字节流,显然是加过密的。client随机生成48字节的Pre-master secret,padding后用public key加密就得到这130字节的数据发送给server,server解密也能得到Pre-master secret。双方使用pre-master secret, “master secret"常量字节流,前期交换的server端RN和client的RN作为参数,使用一个伪随机函数PRF,其实就是hash之后再hash,最后得到48字节的master secret。master secret再与"key expansion"常量,双方RN经过伪随机函数运算得到key_block,PRF伪随机函数可以可以仿佛循环输出数据,因此我们想得到多少字节都可以,就如Random伪随机函数,给它一个种子,后续用hash计算能得到无数个随机数,如果每次种子相同,得到的序列是一样的,但是这里的输入时48字节的master secret,2个28字节的RN和一个字符串常量,碰撞的可能性是很小的。得到key block后,算法,就从中取出session key,IV(对称算法中使用的初始化向量)等。client和server使用的session key是不一样的,但只要双方都知道对方使用的是什么就行了。这里会取出4个:client/server加密正文的key,client/server计算handshake数据hash的key。

6.Client发送ChangeCipherSpec

client指示Server从现在开始发送的消息都是加密过的

7.Client发送Finished

client发送的加密数据,这个消息非常关键,一是能证明握手数据没有被篡改过,二也能证明自己确实是密钥的拥有者(这里是单边验证,只有server有certificate,server发送的Finished能证明自己含有private key,原理是一样的)。client将之前发送的所有握手消息存入handshake messages缓存,进行MD5和SHA-1两种hash运算,再与前面的master secret和一串常量"client finished"进行PRF伪随机运算得到12字节的verify data,还要经过改进的MD5计算得到加密信息。为什么能证明上述两点呢,前面说了只有密钥的拥有者才能解密得到pre-master key,master key,最后得到key block后,进行hash运算得到的结果才与发送方的一致。

8.Server发送ChangeCipherSpec

Server指示client从现在开始发送的消息都是加密过的

9.Server发送Finishd

与client发送Finished计算方法一致。server发送的Finished里包含hash给client,client会进行校验,如果通过,说明握手过程中的数据没有被第三方篡改过,也说明server是之前交换证书的拥有者。现在双方就可以开始后续通信,进入Application context了。

Linux网络栈之队列

https://zhensheng.im/2017/08/11/%e7%bf%bb%e8%af%91linux%e7%bd%91%e7%bb%9c%e6%a0%88%e4%b9%8b%e9%98%9f%e5%88%97.meow

数据包队列是任何一个网络栈的核心组件,数据包队列实现了异步模块之间的通讯,提升了网络性能,并且拥有影响延迟的副作用。本文的目标,是解释Linux的网络栈中IP数据包在何处排队,新的延迟降低技术如BQL是多么的有趣,以及如何控制缓冲区以降低延迟。

下面这张图片将会贯穿全文,其多个修改版本将会用来解释一些特别的概念。

图片一 – Simplified high level overview of the queues on the transmit path of the Linux 网络栈

驱动队列(Driver Queue,又名环形缓冲)

在内核的IP stack和网络接口控制器(NIC)之间,存在一个驱动队列。这个队列典型地以一个先进先出的环形缓冲区实现 —— 即一个固定大小的缓冲区。驱动队列并不附带数据包数据,而是持有指向内核中名为socket kernel buffers(SKBs)的结构体的描述符,SKBs持有数据包的数据并且在整个内核中使用。

图片 2 – Partially full driver queue with descriptors pointing to SKBs

驱动队列的输入来源是一个为所有数据包排队的IP stack,这些数据包可能是本地生成,或者在一个路由器上,由一个NIC接收然后选路从另一个NIC发出。数据包从IP stack入队到驱动队列后,将会被驱动程序执行出队操作,然后通过数据总线进行传输。

驱动队列之所以存在,是为了保证系统无论在任何需要传输数据, NIC都能立即传输。换言之,驱动队列从硬件上给予了IP stack一个异步数据排队的地方。一个可选的方式是当NIC可以传输数据时,主动向IP stack索取数据,但这种设计模式下,无法实时对NIC响应,浪费了珍贵的传输机会,损失了网络吞吐量。另一个与此相反的方法是IP stack创建一个数据包后,需要同步等待NIC,直到NIC可以发送数据包,这也不是一个好的设计模式,因为在同步等待的过程中IP stack无法执行其它工作。

巨型数据包

绝大多数的NIC都拥有一个固定的最大传输单元(MTU),意思是物理媒介可以传输的最大帧。以太网默认的MTU是1,500字节,但一些以太网络支持上限9,000字节的巨型帧(Jumbo Frames)。在IP 网络栈中,MTU描述了一个可被传输的数据包大小上限。例如,一个应用程序通过TCP socket发送了2,000字节的数据,IP stack就需要把这份数据拆分成数个数据包,以保持单个数据包的小于或等于MTU(1,500)。传输大量数据时,小的MTU将会产生更多分包。

为了避免大量数据包排队,Linux内核实现了数个优化:TCP segmentation offload (TSO), UDP fragmentation offload (UFO) 和 generic segmentation offload (GSO),这些优化机制允许IP stack创建大于出口NIC MTU的数据包。以IPv4为例,可以创建上限为65,536字节的数据包,并且可以入队到驱动队列。在TSO和UFO中,NIC在硬件上实现并负责拆分大数据包,以适合在物理链路上传输。对于没有TSO和UFO支持的NIC,GSO则在软件上实现同样的功能。

前文提到,驱动队列只有固定容量,只能存放固定数量的描述符,由于TSO,UFO和GSO的特性,使得大型的数据包可以加入到驱动队列当中,从而间接地增加了队列的容量。图三与图二的比较,解释了这个概念。

图片 3 – Large packets can be sent to the NIC when TSO, UFO or GSO are enabled. This can greatly increase the number of bytes in the driver queue.

虽然本文的其余部分重点介绍传输路径,但值得注意的是Linux也有工作方式像TSO,UFO和GSO的接收端优化。这些优化的目标也是减少每一个数据包的开销。特别地,generic receive offload (GRO)允许NIC驱动把接收到的数据包组合成一个大型数据包,然后加入IP stack。在转发数据包的时候,为了维护端对端IP数据包的特性,GRO会重新组合接收到的数据包。然而,这只是单端效果,当大型数据包在转发方处拆分时,将会出现多个数据包一次性入队的情况,这种数据包"微型突发"会给网络延迟带来负面影响。

饥饿与延迟

先不讨论必要性与优点,在IP stack和硬件之间的队列描述了两个问题:饥饿与延迟。

如果NIC驱动程序要处理队列,此时队列为空,NIC将会失去一个传输数据的机会,导致系统的生产量降低。这种情况定义为饥饿。需要注意:当操作系统没有任何数据需要传输时,队列为空的话,并不归类为饥饿,而是正常。为了避免饥饿,IP stack在填充驱动队列的同时,NIC驱动程序也要进行出队操作。糟糕的是,队列填满或为空的事件持续的时间会随着系统和外部的情况而变化。例如,在一个繁忙的操作系统上,IP stack很少有机会往驱动队列中入队数据包,这样有很大的几率出现驱动队列为空的情况。拥有一个大容量的驱动队列缓冲区,有利于减少饥饿的几率,提高网络吞吐量。

虽然一个大的队列有利于增加吞吐量,但缺点也很明显:提高了延迟。

图片 4 – Interactive packet (yellow) behind bulk flow packets (blue)

图片4展示了驱动队列几乎被单个高流量(蓝色)的TCP段填满。队列中最后一个数据包来自VoIP或者游戏(黄色)。交互式应用,例如VoIP或游戏会在固定的间隔发送小数据包,占用大量带宽的数据传输会使用高数据包传输速率传输大量数据包,高速率的数据包传输将会在交互式数据包之间插入大量数据包,从而导致这些交互式数据包延迟传输。为了进一步解释这种情况,假设有如下场景:

一个网络接口拥有5 Mbit/sec(或5,000,000 bit/sec)的传输能力

每一个大流量的数据包都是1,500 bytes或12,000 bits。

每一个交互式数据包都是500 bytes。

驱动队列的长度为128。

有127个大流量数据包,还有1个交互式数据包排在队列末尾。

在上述情况下,发送127个大流量的数据包,需要(127 * 12,000) / 5,000,000 = 0.304 秒(以ping的方式来看,延迟值为304毫秒)。如此高的延迟,对于交互式程序来说是无法接受的,然而这还没计算往返时间。前文提到,通过TSO,UFO,GSO技术,大型数据包还可以在队列中排队,这将导致延迟问题更严重。

大的延迟,一般由过大、疏于管理的缓冲区造成,如Bufferbloat。更多关于此现象的细节,可以查阅控制队列延迟(Controlling Queue Delay),以及Bufferbloat项目。

如上所述,为驱动队列选择一个合适的容量是一个Goldilocks问题 – 这个值不能太小,否则损失吞吐量,也不能太大,否则过增延迟。

字节级队列限制(Byte Queue Limits (BQL))

Byte Queue Limits (BQL)是一个在Linux Kernel 3.3.0加入的新特性,以自动解决驱动队列容量问题。BQL通过添加一个协议,计算出的当前情况下避免饥饿的最小数据包缓冲区大小,以决定是否允许继续向驱动队列中入队数据包。根据前文,排队的数据包越少,数据包排队的最大发送延迟就越低。

需要注意,驱动队列的容量并不能被BQL修改,BQL做的只是计算出一个限制值,表示当时有多少的数据可以被排队。任何超过此限制的数据,是等待还是被丢弃,会根据协议而定。

BQL机制在以下两种事件发生时将会触发:数据包入队,数据包传输完成。一个简化的BQL算法版本概括如下IMIT为BQL根据当前情况计算出的限制值。

1
2
3
4
5
6
****
** 数据包入驱动队列后
****

如果队列排队数据包的总数据量超过当前限制值
禁止数据包入驱动队列

这里要清楚,被排队的数据量可以超过LIMIT,因为在TSO,UFO或GSO启用的情况下,一个大型的数据包可以通过单个操作入队,因此LIMIT检查在入队之后才发生,如果你很注重延迟,那么可能需要考虑关闭这些功能,本文后面将会提到如何实现这个需求。

BQL的第二个阶段在硬件完成数据传输后触发(pseudo-code简化版):

1
2
3
4
5
6
7
8
9
10
11
12
13
****
** 当硬件已经完成一批次数据包的发送
** (一个周期结束)
****

如果硬件在一个周期内处于饥饿状态
提高LIMIT

否则,如果硬件在一个周期内都没有进入饥饿状态,并且仍然有数据需要发送
使LIMIT减少"本周期内留下未发送的数据量"

如果驱动队列中排队的数据量小于LIMIT
  允许数据包入驱动队列

如你所见,BQL是以测试设备是否被饥饿为基础实现的。如果设备被饥饿,LIMIT值将会增加,允许更多的数据排队,以减少饥饿,如果设备整个周期内都处于忙碌状态并且队列中仍然有数据需要传输,表明队列容量大于当前系统所需,LIMIT值将会降低,以避免延迟的提升。

BQL对数据排队的影响效果如何?一个真实世界的案例也许可以给你一个感觉。我的一个服务器的驱动队列大小为256个描述符,MTU 1,500字节,意味着最多能有256 * 1,500 = 384,000字节同时排队(TSO,GSO之类的已被关闭,否则这个值将会更高)。然而,由BQL计算的限制值是3,012字节。如你所见,BQL大大地限制了排队数据量。

BQL的一个有趣方面可以从它名字的第一个词思议——byte(字节)。不像驱动队列和大多数的队列容量,BQL直接操作字节,这是因为字节数与数据包数量相比,能更有效地影响数据传输的延迟。

BQL通过限制排队的数据量为避免饥饿的最小需求值以降低网络延迟。对于移动大量在入口NIC的驱动队列处排队的数据包到queueing discipline(QDisc)层,BQL起到了非常重要的影响。QDisc层实现了更复杂的排队策略,下一节介绍Linux QDisc层。

排队规则(Queuing Disciplines (QDisc))

驱动队列是一个很简单的先进先出(FIFO)队列,它平等对待所有数据包,没有区分不同流量数据包的功能。这样的设计优点是保持了驱动程序的简单以及高效。要注意更多高级的以太网络适配器以及绝大多数的无线网络适配器支持多种独立的传输队列,但同样的都是典型的FIFO。较高层的负责选择需要使用的传输队列。

在IP stack和驱动队列之间的是排队规则(queueing discipline(QDisc))层(见图1)。这一层实现了内核的流量管理能力,如流量分类,优先级和速率调整。QDisc层通过一些不透明的tc命令进行配置。QDisc层有三个关键的概念需要理解:QDiscs,classes(类)和filters(过滤器)。

QDisc是Linux对流量队列的一个抽象化,比标准的FIFO队列要复杂得多。这个接口允许QDisc提供复杂的队列管理机制而无需修改IP stack或者NIC驱动。默认地,每一个网络接口都被分配了一个pfifo_fast QDisc,这是一个实现了简单的三频优先方案的队列,排序以数据包的TOS位为基础。尽管这是默认的,pfifo_fast QDisc离最佳选择还很远,因为它默认拥有一个很深的队列(见下文的txqueuelen)并且无法区分流量。

第二个与QDisc关系很密切的概念是类,独立的QDiscs为了以不同方式处理子集流量,可能实现类。例如,分层令牌桶(Hierarchical Token Bucket (HTB))QDisc允许用户配置一个500 Kbps和300 Kbps的类,然后根据需要,把流量归为特定类。需要注意,并非所有QDiscs拥有对多个类的支持——那些被称为类的QDiscs。

过滤器(也被称为分类器),是一个用于流量分类到特定QDisc或类的机制。各种不同的过滤器复杂度不一,u32是一个最通用的也可能是一个最易用的流量过滤器。流量过滤器的文档比较缺乏,不过你可以在此找到使用例子:我的一个QoS脚本。

更多关于QDiscs,classes和filters的信息,可阅LARTC HOWTO,以及tc的man pages。

传输层与排队规则间的缓冲区

在前面的图片中,你可能会发现排队规则层并没有数据包队列。这意思是,网络栈直接放置数据包到排队规则中或者当队列已满时直接放回到更上层(例如socket缓冲区)。这很明显的一个问题是,如果接下来有大量数据需要发送,会发送什么?这种情况会在TCP链接发生大量堵塞或者甚至有些应用程序以其最快的速度发送UDP数据包时出现。对于一个持有单个队列的QDisc,与图4中驱动队列同样的问题将会发生,亦即单个大带宽或者高数据包传输速率流会把整个队列的空间消耗完毕,从而导致丢包,极大影响其它流的延迟。更糟糕的是,这产生了另一个缓冲点,其中可以形成standing queue,使得延迟增加并导致了TCP的RTT和拥塞窗口大小计算问题。Linux默认的pfifo_fast QDisc,由于大多数数据包TOS标记为0,因此基本可以视作单个队列,因此这种现象并不罕见。

Linux 3.6.0(2012-09-30),加入了一个新的特性,称为TCP小型队列,目标是解决上述问题。TCP小型队列限制了每个TCP流每次可在QDisc与驱动队列中排队的字节数。这有一个有趣的影响:内核会更早调度回应用程序,从而允许应用程序以更高效的优先级写入套接字。目前(2012-12-28),其它单个传输流仍然有可能淹没QDisc层。

另一个解决传输层洪水问题的方案是使用具有多个队列的QDisc,理想情况下每个网络流一个队列。随机公平队列(Stochastic Fairness Queueing (SFQ))和延迟控制公平队列(Fair Queueing with Controlled Delay (fq_codel))都有为每个网络流分配一个队列的机制,因此很适合解决这个洪水问题。

如何控制Linux的队列容量

驱动队列

ethtool命令可用于控制以太网设备驱动队列容量。ethtool也提供了底层接口分析,可以启用或关闭IP stack和设备的一些特性。

-g参数可以输出驱动队列的信息:

1
2
3
4
5
6
7
8
9
10
11
12
[root@alpha net-next]# ethtool -g eth0
Ring parameters for eth0:
Pre-set maximums:
RX:        16384
RX Mini:    0
RX Jumbo:    0
TX:        16384
Current hardware settings:
RX:        512
RX Mini:    0
RX Jumbo:    0
TX:        256

你可以从以上的输出看到本NIC的驱动程序默认拥有一个容量为256描述符的传输队列。早期,在Bufferbloat的探索中,这个队列的容量经常建议减少以降低延迟。随着BQL的使用(假设你的驱动程序支持它),再也没有任何必要去修改驱动队列的容量了(如何配置BQL见下文)。

Ethtool也允许你管理优化特性,例如TSO,UFO和GSO。-k参数输出当前的offload设置,-K修改它们。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[dan@alpha ~]$ ethtool -k eth0
Offload parameters for eth0:
rx-checksumming: off
tx-checksumming: off
scatter-gather: off
tcp-segmentation-offload: off
udp-fragmentation-offload: off
generic-segmentation-offload: off
generic-receive-offload: on
large-receive-offload: off
rx-vlan-offload: off
tx-vlan-offload: off
ntuple-filters: off
receive-hashing: off

由于TSO,GSO,UFO和GRO极大的提高了驱动队列中可以排队的字节数,如果你想优化延迟而不是吞吐量,那么你应该关闭这些特性。如果禁用这些特性,除非系统正在处理非常高的数据速率,否则您将不会注意到任何CPU影响或吞吐量降低。

Byte Queue Limits (BQL)

BQL是一个自适应算法,因此一般来说你不需要为此操心。然而,如果你想牺牲数据速率以换得最优延迟,你就需要修改LIMIT的上限值。BQL的状态和设置可以在/sys中NIC的目录找到,在我的服务器上,eth0的BQL目录是:

1
/sys/devices/pci0000:00/0000:00:14.0/net/eth0/queues/tx-0/byte_queue_limits

在该目录下的文件有:

hold_time: 修改LIMIT值的时间间隔,单位为毫秒

inflight: 还没发送且在排队的数据量

limit: BQL计算的LIMIT值,如果NIC驱动不支持BQL,值为0

limit_max: LIMIT的最大值,降低此值可以优化延迟

limit_min: LIMIT的最小值,增高此值可以优化吞吐量

要修改LIMIT的上限值,把你需要的值写入limit_max文件即可,单位为字节:

1
echo "3000" > limit_max

什么是txqueuelen?

在早期的Bufferbload讨论中,经常会提到静态地减少NIC传输队列长度。当前队列长度值可以通过ip和ifconfig命令取得。令人疑惑的是,这两个命令给了传输队列的长度不同的名字:

1
2
3
4
5
6
7
8
9
10
[dan@alpha ~]$ ifconfig eth0
eth0      Link encap:Ethernet  HWaddr 00:18:F3:51:44:10
          inet addr:69.41.199.58  Bcast:69.41.199.63  Mask:255.255.255.248
          inet6 addr: fe80::218:f3ff:fe51:4410/64 Scope:Link
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
          RX packets:435033 errors:0 dropped:0 overruns:0 frame:0
          TX packets:429919 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:1000
          RX bytes:65651219 (62.6 MiB)  TX bytes:132143593 (126.0 MiB)
          Interrupt:23
1
2
3
4
5
 [dan@alpha ~]$ ip link
1: lo:  mtu 16436 qdisc noqueue state UNKNOWN
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
2: eth0:  mtu 1500 qdisc pfifo_fast state UP qlen 1000
    link/ether 00:18:f3:51:44:10 brd ff:ff:ff:ff:ff:ff

Linux默认的传输队列长度为1,000个数据包,这是一个很大的缓冲区,尤其在低带宽的情况下。

有趣的问题是,这个变量实际上是控制什么?

我也不清楚,因此我花了点时间深入探索内核源码。我现在能说的,txqueuelen只是用来作为一些排队规则的默认队列长度。例如:

1
2
3
4
5
6
7
pfifo_fast(Linux默认排队规则)
sch_fifo
sch_gred
sch_htb(只有默认队列)
sch_plug
sch_sfb
sch_teql

见图1,txqueuelen参数在排队规则中控制以上列出的队列类型的长度。绝大多数这些排队规则,tc的limit参数默认会覆盖掉txqueuelen。总的来说,如果你不是使用上述的排队规则,或者如果你用limit参数指定了队列长度,那么txqueuelen值就没有任何作用。

顺便一提,我发现一个令人疑惑的地方,ifconfig命令显示了网络接口的底层信息,例如MAC地址,但是txqueuelen却是来自高层的QDisc层,很自然的地,看起来ifconfig会输出驱动队列长度。

传输队列的长度可以使用ip或ifconfig命令修改:

1
[root@alpha dan]# ip link set txqueuelen 500 dev eth0

需要注意,ip命令使用"txqueuelen"但是输出时使用"qlen" —— 另一个不幸的不一致性。

排队规则

正如前文所描述,Linux内核拥有大量的排队规则(QDiscs),每一个都实现了自己的数据包排队方法。讨论如何配置每一个QDiscs已经超出了本文的范围。关于配置这些队列的信息,可以查阅tc的man page(man tc)。你可以使用"man tc qdisc-name"(例如:"man tc htb"或"man tc fq_codel")找到每一个队列的细节。LARTC也是一个很有用的资源,但是缺乏了一些新特性的信息。

以下是一些可能对你使用tc命令有用的建议和技巧:

HTB QDisc实现了一个接收所有未分类数据包的默认队列。一些如DRR QDiscs会直接把未分类的数据包丢进黑洞。使用命令"tc qdisc show",通过direct_packets_stat可以检查有多少数据包未被合适分类。

HTB类分层只适用于分类,对于带宽分配无效。所有带宽分配通过检查Leaves和它们的优先级进行。

QDisc中,使用一个major和一个minor数字作为QDiscs和classes的基本标识,major和minor之间使用英文冒号分隔。tc命令使用十六进制代表这些数字。由于很多字符串,例如10,在十进制和十六进制都是正确的,因此很多用户不知道tc使用十六进制。见我的tc脚本,可以查看我是如何处理这个问题的。

如果你正在使用基于ATM的ADSL(绝大多数的DLS服务是基于ATM,新的变体例如VDSL2可能不是),你很可能需要考虑添加一个"linklayer adsl"的选项。这个统计把IP数据包分解成一组53字节的ATM单元所产生的开销。

如果你正在使用PPPoE,你很可能需要考虑通过"overhead"参数统计PPPoE开销。

TCP小型队列

每个TCP Socket的队列限制可以通过/proc中的文件查看或修改:

1
/proc/sys/net/ipv4/tcp_limit_output_bytes