TCP拥塞控制算法BBR的原理和改进实践

建议就着咖啡观看,避免干着 实验环境 操作系统:Debian 10 Linux Kernel:5.10.x 前言 我在编写之前的文章《服务器性能优化之网络性能优化》中,其实有想把 BBR 作为一种优化手段写进去的,然而受限于篇幅不能过多讲解,因此特地单独细致地研究一遍。 通常我们在研究 BBR 算法的时候,会很难不将其与 Cublic 算法做比较,作为 Linux 内核默认的 TCP 拥塞控制算法,我们在这里也将会一并列入对比和实验中。 基本的概念 盖房先建地基,对应的学习一门知识也要先掌握与巩固前置基础,这里先将几个较为关键和频繁出现的知识要点来回顾一下,但在这里我将默认你已经掌握了基本的网络技术(例如滑动窗口,重传定时器等概念),否则你更需要的可能是一本网络入门书。 拥塞控制四板斧 由于 TCP 协议向应用层提供不定长的字节流发送方法,使得 TCP 协议先天性地有意愿去占满网络中的整个带宽,这时候当网络中许多连接同时试图去占满整个带宽的时候,就有可能发生恶性拥塞事件,因此 TCP 拥塞控制算法的作用,它能有效防止过多的数据注入到网络中,导致出现网络负载过大的情况。当 TCP 拥塞控制算法无法满足当前互联网应用对网络传输高实时性、高带宽利用率、高吞吐量的需求,在这种背景下 BBR 应运而生。 注意它和流量控制的区别,流量控制更多是作用于接收者,它控制发送者的发送速度从而使接收者来得及接收,防止数据分组丢失 在我们逐步分析对应控制手段之前,需要先行假设: 数据是单方向传递,另一个窗口只发送确认 接收方的缓存足够大,因此发送方的窗口大小由网络的拥塞程度来决定 慢启动 概念 我们都有过从互联网下载东西的经历,通常来说,文件的下载速率并不是一开始就达到到你的宽带上限,而是逐步从例如 1MB/s → 4MB/s 递增上去,这其实就是我们能看见的一种很典型的慢启动策略。 在这场下载中,发送方(即内容提供服务器)会维持一个叫做拥塞窗口 cwnd 的状态变量。它用来表示发送方在得到接收方确认前,最大允许传输的未经确认的窗口。拥塞窗口的大小取决于网络的拥塞程度,并且动态地在变化。 这时候我们又要引入两个新的词,即 通告窗口 rwnd 发送窗口 swnd = min(cwnd, rwnd) 虽然是新的词,但概念大体是新瓶装老酒,通告窗口就是我们 TCP 报文头的 Window 字段,也就是对方的接收窗口,例如我这里截了一张随手抓的报文内容,这里的 rwnd 就为 1021 而发送窗口的概念就更简单了,它将 rwnd 与 cwnd 做比较,取两者最小值,如果我们将整条连接堪称一个水桶,那么它代表水桶中最短的那块木板,而 cwnd 与 rwnd 相比不同的是:它只是发送方的一个内部记数,无需通知给接收方,其初始值往往会比较小,然后随着报文被接收方确认,窗口成倍扩大,有点类似拳击比赛,开始时候不了解对方情况只能先行试探,后来心里有底了逐渐加大进攻力度。 ...

March 2, 2022 · 21 min · Sxueck

使用Lua编写插件加强Nginx - 初探

虽然说 Nginx 的标准模块非常强大,然而如果遇到了一些不能灵活适应系统要求的功能时候,我们往往会考虑使用 Lua 拓展和定制 Nginx 服务,目前对于这种需要拓展性的项目一般采用 OpenResty ,按照官方的介绍,OpenResty 是一个基于 Nginx 与 Lua 的高性能 Web 平台,其内部集成了大量精良的 Lua 库、第三方模块以及大多数的依赖项。用于方便地搭建能够处理超高并发、扩展性极高的动态Web 应用、Web 服务和动态网关 但是,有的时候服务已经部署完成,包括配置都已经写好了,直接对线上的 Nginx 进行替换显然是一种非常冒险与费神的行为,又或者是只需要某一项功能的拓展,并不需要整体做大改动,这个时候用 Lua 模块就是一种很好的选择。 模拟线上环境 这里我启用了虚拟机作为线上的机器,因此需要重新安装 Nginx,Arguments 这边是需要单独记下来的,和后面编译做到尽量一个参数,这样对配置文件不会造成任何影响 $ sudo add-apt-repository ppa:nginx/stable $ sudo apt-get update $ sudo apt install nginx $ nginx -V nginx version: nginx/1.18.0 (Ubuntu) built with OpenSSL 1.1.1f 31 Mar 2020 TLS SNI support enabled configure arguments: --with-cc-opt='-g -O2 -fdebug-prefix-map=/build/nginx-d4S6DH/nginx-1.18.0=. -fstack-protector-strong -Wformat -Werror=format-security -fPIC -Wdate-time -D_FORTIFY_SOURCE=2' --with-ld-opt='-Wl,-Bsymbolic-functions -Wl,-z,relro -Wl,-z,now -fPIC' --prefix=/usr/share/nginx --conf-path=/etc/nginx/nginx.conf --http-log-path=/var/log/nginx/access.log --error-log-path=/var/log/nginx/error.log --lock-path=/var/lock/nginx.lock --pid-path=/run/nginx.pid --modules-path=/usr/lib/nginx/modules --http-client-body-temp-path=/var/lib/nginx/body --http-fastcgi-temp-path=/var/lib/nginx/fastcgi --http-proxy-temp-path=/var/lib/nginx/proxy --http-scgi-temp-path=/var/lib/nginx/scgi --http-uwsgi-temp-path=/var/lib/nginx/uwsgi --with-compat --with-debug --with-pcre-jit --with-http_ssl_module --with-http_stub_status_module --with-http_realip_module --with-http_auth_request_module --with-http_v2_module --with-http_dav_module --with-http_slice_module --with-threads --with-http_addition_module --with-http_gunzip_module --with-http_gzip_static_module --with-http_sub_module 编译包含了 Lua 模块的 Nginx $ mkdir nginx && cd nginx $ wget https://nginx.org/download/nginx-1.18.0.tar.gz $ tar xvf nginx-1.18.0.tar.gz 不推荐去 LuaJIT 官网下载 2.0.5 版本,不然在 arm64 下将无法进行编译(M1 Mac) ...

February 23, 2022 · 3 min · Sxueck

从GoProxy中学习反向代理

阅读目的 弄清楚项目是如何完成代理的功能,因此重点弄懂 Proxy 部分的代码 项目地址 https://github.com/goproxyio/goproxy 从项目启动开始,我们的重点就是下面的这个参数 flag.StringVar(&proxyHost, "proxy", "", "next hop proxy for Go Modules, recommend use https://gopropxy.io") 我们将这个参数设置为国内可以访问的 https://goproxy.cn ,并且尝试一下是否能跑通整个服务,这里使用的是 Powershell,换成 Linux Shell 的时候其实也是一样的流程 # 单独开启一个终端启动服务 $ $CacheDIR = "G:\\GomodDebug" $ mkdir -p $CacheDIR\pkg\mod\cache\download\github.com\goproxyio\goproxy\@v\ $ go run . -proxy https://goproxy.cn -listen 127.0.0.1:8085 -cacheDir $CacheDIR goproxy.io: ProxyHost https://goproxy.cn goproxy.io: ------ --- /github.com/@v/list [proxy] goproxy.io: ------ --- /github.com/goproxyio/goproxy/@v/list [proxy] goproxy.io: ------ --- /github.com/goproxyio/@v/list [proxy] goproxy.io: 0.344s 404 /github.com/@v/list goproxy.io: 0.548s 200 /github.com/goproxyio/goproxy/@v/list goproxy.io: 0.619s 404 /github.com/goproxyio/@v/list goproxy.io: ------ --- /github.com/goproxyio/goproxy/internal/@v/list [proxy] goproxy.io: ------ --- /github.com/goproxyio/goproxy/internal/cfg/@v/list [proxy] goproxy.io: ------ --- /github.com/goproxyio/goproxy/internal/module/@v/list [proxy] goproxy.io: ------ --- /github.com/goproxyio/goproxy/internal/modfetch/@v/list [proxy] goproxy.io: ------ --- /github.com/goproxyio/goproxy/internal/modfetch/codehost/@v/list [proxy] goproxy.io: ------ --- /github.com/goproxyio/goproxy/internal/modload/@v/list [proxy] goproxy.io: 0.428s 404 /github.com/goproxyio/goproxy/internal/@v/list goproxy.io: 0.473s 404 /github.com/goproxyio/goproxy/internal/module/@v/list goproxy.io: 0.532s 404 /github.com/goproxyio/goproxy/internal/modfetch/@v/list goproxy.io: 0.555s 404 /github.com/goproxyio/goproxy/internal/modload/@v/list goproxy.io: 0.592s 404 /github.com/goproxyio/goproxy/internal/cfg/@v/list goproxy.io: 0.697s 404 /github.com/goproxyio/goproxy/internal/modfetch/codehost/@v/list # 这是IDE中正常的测试终端 $ go env -w GO111MODULE=on $ go env -w GOPROXY=http://127.0.0.1:8085 # 我们上面并没有设置direct,意味着拉取源只会通过代理 $ go get -u github.com/goproxyio/goproxy github.com/goproxyio/goproxy imports github.com/goproxyio/goproxy/pkg/proxy imports ... 深入研究Proxy的流程 Director handle = &logger{proxy.NewRouter(proxy.NewServer(new(ops)), &proxy.RouterOptions{ Pattern: excludeHost, Proxy: proxyHost, DownloadRoot: downloadRoot, CacheExpire: cacheExpire, })} func NewRouter(srv *Server, opts *RouterOptions) *Router { rt := &Router{ opts: opts, srv: srv, } if opts != nil { if opts.Proxy == "" { log.Printf("not set proxy, all direct.") return rt } remote, err := url.Parse(opts.Proxy) if err != nil { log.Printf("parse proxy fail, all direct.") return rt } proxy := httputil.NewSingleHostReverseProxy(remote) director := proxy.Director proxy.Director = func(r *http.Request) { director(r) r.Host = remote.Host } rt.proxy = proxy rt.proxy.Transport = &http.Transport{ Proxy: http.ProxyFromEnvironment, TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, } rt.proxy.ModifyResponse = rt.customModResponse ... } return rt } 如果没有设置代理或者代理没有通过 url.Parse 的解析 ,服务将会提示将会使用直连模式 ...

February 11, 2022 · 4 min · Sxueck

如何编写一款网络代理服务

本篇文章仅供技术交流,请遵循相关国家法律法规,作者不提供任何技术支持 没啥思路,文章暂时搁置 - 2022-03-02 前言 Socks5 协议 在编写相关代码之前,我们需要先将 Socks5 协议给了解一下 使用 WireShark 抓取 Socks5 报文 $ curl --socks5 157.90.140.29:1080 ip.sb 同时我也将 WireShark 抓取到的报文片段截取下来了,可以点击 下载 后,使用相同的软件进行本地分析 分析Go-Shadowsocks2的源码 tcp.go // Create a SOCKS server listening on addr and proxy to server. func socksLocal(addr, server string, shadow func(net.Conn) net.Conn) { logf("SOCKS proxy %s <-> %s", addr, server) tcpLocal(addr, server, shadow, func(c net.Conn) (socks.Addr, error) { return socks.Handshake(c) }) } // Create a TCP tunnel from addr to target via server. func tcpTun(addr, server, target string, shadow func(net.Conn) net.Conn) { tgt := socks.ParseAddr(target) if tgt == nil { logf("invalid target address %q", target) return } logf("TCP tunnel %s <-> %s <-> %s", addr, server, target) tcpLocal(addr, server, shadow, func(net.Conn) (socks.Addr, error) { return tgt, nil }) } addr:客户端监听地址 server:客户端连接的地址,也就是远程服务器的地址 而关于 shadow 这个传入的匿名函数,我们可以通过其他地方是如何调用它的来进行作用判断 ...

January 24, 2022 · 2 min · Sxueck

经验杂谈

这里汇集了平常学习和工作中遇到的一些小小的疑难杂症 Linux Service Nginx xxx.so is not binary compatible in /etc/nginx/nginx.conf 这个通常发生在编译 Nginx 模块后,我们通过 load_module 加载该模块发出的报错,针对这种的解决方案也很简单,加上 --with-compat 即可,例如 ./configure --with-http_image_filter_module=dynamic --with-compat Kubernetes 数据库中间件问题 MongoDB ReplicaSetNoPrimary 问题 错误日志: server selection error: server selection timeout, current topology: { Type: ReplicaSetNoPrimary, Servers: [{ Addr: xxx:27017, Type: Unknown, Last error: connection() error occured during connection handshake: connection(xxx:27017[-127]) socket was unexpectedly closed: EOF }, { Addr: xxx:27017, Type: Unknown, Last error: connection() error occured during connection handshake: connection(xxx:27017[-128]) socket was unexpectedly closed: EOF }, ] } 从表面上看,代码 SDK 连接到了 Mongo 后,Mongo 返回来一个 ReplicaSet 集合,其中里面没有我可以访问的地址(返回了集群 POD 的 Name),其主要的原因是使用的 SDK 较老,那时候还没有集群化管理这种东西,不具备服务发现的功能 解决方案: In contrast to the mongo-go-driver, by default it would perform server discovery and attempt to connect as a replica set. If you would like to connect as a single node, then you need to specify connect=direct in the connection URI. 例如,在 PyMongo 的连接中加入 directConnection = True,或者类似方案 ...

January 24, 2022 · 4 min · Sxueck

服务器性能优化之网络性能优化

这篇文章,将会从网络原理开始,一直到 Linux Kernel 的相关配置对于网络性能的影响,算是一个偏整体的知识学习 文章非常长,这里将大纲列出来 网卡收发包队列技术优化 单网卡队列 RPS / RFS 原理 TCP Socket 建立连接相关内核参数优化 TCP 连接拥塞控制相关内核参数优化 TCP 拥塞控制算法 - BBRv2 原理 TCP 显式拥塞通知 ECN 算法原理 TCP 快速启动解决方案 概念梳理 学习过程中可能会碰到非常多的概念和术语,在此先对这些进行一个梳理,也算是一个复习 中断和软中断 首先我们需要对中断和软中断有个起码的认知,中断一般是指硬件中断,多由系统自身或与之链接的外设(如键盘、鼠标、网卡等)产生,中断首先是处理器提供的一种响应外设请求的机制,是处理器硬件支持的特性,一个外设通过产生一种电信号通知中断控制器,中断控制器再向处理器发送相应的信号。处理器检测到了这个信号后就会打断自己当前正在做的工作,转而去处理这次中断。而内核收到信号后会调用一个称为中断处理程序(interrupt handler)或中断服务例程(interrupt service routine)的特定程序。中断处理程序或中断服务例程可以在中断向量表中找到,这个中断向量表位于内存中的固定地址中。CPU处理中断后,就会恢复执行之前被中断的程序。 整个流程可以归纳为:硬件设备 (产生中断)-> 中断控制器(通知) -> CPU -> 中断内核 -> do_IRQ() -> handler_IRQ_event -> 中断控制器(收到恢复通知)-> IRQ_exit 现在我们对于基本的中断有了个理解,从实际场景来说中断请求的处理程序应该要短且快,这样才能减少对正常进程运行调度地影响,而且中断处理程序可能会暂时关闭中断,这时如果中断处理程序执行时间过长,可能在还未执行完中断处理程序前,会丢失当前其他设备的中断请求。 所以 Linux 系统为了解决中断处理程序执行过长和中断丢失的问题,将中断过程分成了两个阶段,分别是 上半部分 和 下半部分 上半部用来快速处理中断,一般会暂时关闭中断请求,主要负责处理跟硬件紧密相关或者时间敏感的事情。 下半部用来延迟处理上半部未完成的工作,一般以 内核线程 的方式运行。 例如网卡收包这里,网卡收到网络包后,会通过硬件中断通知内核有新的数据到了,于是内核就会调用对应的中断处理程序来响应该事件,这个事件的处理也是会分成上半部和下半部。 上部分要做到快速处理,所以只要把网卡的数据读到内存中,然后更新一下硬件寄存器的状态,比如把状态更新为表示数据已经读到内存中的状态值。接着,内核会触发一个软中断,把一些处理比较耗时且复杂的事情,交给「软中断处理程序」去做,也就是中断的下半部,其主要是需要从内存中找到网络数据,再按照网络协议栈,对网络数据进行逐层解析和处理,最后把数据送给应用程序。 所以,中断处理程序的上部分和下半部可以理解为: 上半部直接处理硬件请求,也就是硬中断,主要是负责耗时短的工作,特点是快速执行 下半部是由内核触发,也就说软中断,主要是负责上半部未完成的工作,通常都是耗时比较长的事情,特点是延迟执行 网卡收发过程 网卡的收发其实重点应该在中断的下半部,我们一拍脑袋,想到接收数据包的一个经典过程: 数据到达网卡 网卡产生一个中断给内核 内核使用I/O指令,从网卡I/O区域中去读取数据 但是这种方法有一个很大的问题,就是当大流量数据到来时候,网卡会产生大量的中断,内核在处理中断上下文中,会浪费大量资源来处理中断本身。所以,NAPI 技术被提出来,所谓的 NAPI 技术,其实就是先将内核屏蔽,然后每隔一段时间去轮询网卡是否有数据。不过相应的,如果数据量少,轮询本身也会占用大量不必要的 CPU 资源,所以需要进行抉择判断。 ...

January 22, 2022 · 10 min · Sxueck

编写一个带 SASL 认证的Kafka镜像

要求说明: Kafka Brokers 不管是互相通信,对外认证,还是和 Zookeeper 的交互进行,都需要使用 SASL/PLAIN 进行认证 思路: Kafka 的 bin 目录下面,包含了 Zookeeper 和 Kafka Server 的启动脚本,我们只需要在脚本运行之前,通过环境变量 KAFKA_OPTS 指定对应的 JAAS 认证文件,最后启动 properties 文件即可,理清了整个环节之后还是非常简单的 操作开始 目录结构 首先需要编写 Dockerfile 作为环境变量声明的地方,不然配置文件就没法使用模板进行编排了 FROM openjdk:8-jre-slim RUN set -eux; \ apt -y update && apt -y install procps; \ apt clean WORKDIR / ENV STORE_DATA="/kafka_2.12-1.1.1/store/data" \ STORE_LOGS="/kafka_2.12-1.1.1/store/logs" \ ZOO_USER="ZookeeperUsername" \ ZOO_PASS="ZookeeperPassword" \ ZOO_QUORUM_USER="QuorumUserName" \ ZOO_QUORUM_PASS="QuorumUserPassword" \ KAFKA_USER="KafkaUsername" \ KAFKA_PASS="KafkaPassword" \ KAFKA_CLIENT_BROKER_USER="KafkaClientBrokerUsername" \ KAFKA_CLIENT_BROKER_PASS="KafkaClientBrokerPassword" \ ZOOKEEPER_CONNECT="localhost:2181" \ MODE="" \ # create multiple users with different permissions # operation attribute: Read Write * # KAFKA_ACCOUNT_GROUP "user1|password|* readonlyuser|password|Read" KAFKA_ACCOUNT_GROUP="" \ # each pod contains a zk and a kafka # so their number of nodes and information should be the same CLUSTER_NODE="" # CLUSTERNODE "node0|0|127.0.0.1 node1|1|127.0.0.1" ENV PATH=$PATH:/kafka_2.12-1.1.1/bin \ ZOO_DATA_DIR="$STORE_DATA/zookeeper/data" \ ZOO_MYID_FILE="$STORE_DATA/zookeeper/data/myid" \ ZOO_DATA_LOG_DIR="$STORE_LOGS/zookeeper/logs/dataLogs" \ ZOO_LOG_DIR="$STORE_LOGS/zookeeper/logs" \ SERVER_JVMFLAGS="-Djava.security.auth.login.config=/kafka_2.12-1.1.1/config/zoo_server_jaas.conf" \ KAFKA_OPTS="-Djava.security.auth.login.config=/kafka_2.12-1.1.1/config/kafka_server_jaas.conf" \ ZOO_CFG="/kafka_2.12-1.1.1/config/zookeeper.properties" \ Z00_AUTH="/kafka_2.12-1.1.1/config/zoo_server_jaas.conf" \ KAFKA_CFG="/kafka_2.12-1.1.1/config/server.properties" \ KAFKA_AUTH="/kafka_2.12-1.1.1/config/kafka_server_jaas.conf" \ KAFKA_DATA_LOG_DIR="$STORE_LOGS/kafka/logs/dataLogs" ENV ENV_CONFIGRUE="ZOO_DATA_DIR ZOO_DATA_LOG_DIR ZOO_USER ZOO_PASS ZOO_QUORUM_USER ZOO_QUORUM_PASS KAFKA_USER KAFKA_PASS KAFKA_CLIENT_BROKER_USER KAFKA_CLIENT_BROKER_PASS KAFKA_DATA_LOG_DIR ZOOKEEPER_CONNECT" \ ENV_CONFIGFILE="ZOO_CFG Z00_AUTH KAFKA_CFG KAFKA_AUTH" \ POD_NAME="" ADD kafka_2.12-1.1.1.tgz . COPY zookeeper.properties server.properties kafka_server_jaas.conf zoo_server_jaas.conf /kafka_2.12-1.1.1/config/ RUN set -eux;\ groupadd -r kafka --gid=1000; \ useradd -r -g kafka --uid=1000 kafka; \ mkdir -p "$ZOO_DATA_DIR" "$ZOO_DATA_LOG_DIR" "$ZOO_LOG_DIR" ; \ chown -R kafka:kafka /kafka_2.12-1.1.1/ COPY --chown=root:root docker-entrypoint.sh pre-stop.sh / USER kafka:kafka CMD ["/bin/bash", "/docker-entrypoint.sh"] 这里声明了我们需要被用到的一些通用环境变量,还有镜像的一个构建方式 ...

January 9, 2022 · 5 min · Sxueck