Nginx-QUIC初尝试,体验HTTP/3


1、背景介绍

QUIC起源

nginx官方从1.19版本开始,新建立了一个分支,专门用来对QUIC进行支持,官网的链接点这里。注意该项目还处于早期的alpha版本,非常不建议用于生产环境。

The code is at an early alpha level of quality and should not be used in production.

nginx-quic的安装包可以在下面这里找到,由于还处于开发阶段,项目更新得非常快。

https://hg.nginx.org/nginx-quic/shortlog/quic

从官网的readme文件我们可以了解到,截止到2020年8月21日,nginx-quic项目目前只支持h3-27h3-28h3-29三个版本,再早期的草案版本并不支持,不过目前更新的速度非常快,变动也很大。

Currently we support IETF-QUIC draft-27, draft-28, draft-29. Earlier drafts are NOT supported as they have incompatible wire format.

考虑到浏览器的兼容性,接下来我们主要基于h3-27版本进行测试。

同时需要注意的是openssl官方版本目前并不支持quic协议,官方于2020年2月给出的原因是目前quic并不稳定,还有很多版本需要迭代确定,它们需要优先把精力放在openssl3.0版本的开发上,等到openssl3.0版本开发完成了再来进行quic的支持。

So in conclusion; QUIC is on our minds, but it will not be included in the OpenSSL 3.0 release. We expect more tangible action to happen after we’ve released OpenSSL 3.0.

所以我们这里需要用到谷歌自己使用的openssl分支版本boringssl,这是谷歌对应的开源的自用的openssl版本,谷歌官方表示boringssl这个项目虽然开源了,但是并不建议大家在生产环境上广泛使用,因为它是根据谷歌自身的需求进行调整的,有些API可能并不稳定。

为什么需要QUIC

TCP 无法绕过的队头阻塞

  • HTTP/2的难处

HTTP/2的其中一个主要特性是使用多路复用(multiplexing),使得它可以通过同一个TCP连接发送多个逻辑数据流。同时它也带来更好的拥塞控制、更充分的带宽利用,链路能更容易实现全速传输。并且通过标头压缩技术也减少了带宽的用量。

采用HTTP/2后,浏览器对每个主机一般只需要一个 TCP 连接即可创建并行的几十个乃至上百个传输,而不是以前常见的 六个 连接。

但如果HTTP/2连接双方的网络中有一个数据包丢失,或者任何一方的网络出现中断,整个TCP连接就会暂停,丢失的数据包需要被重新传输。因为TCP是一个按序传输的链条,因此如果其中一个点丢失了,链路上之后的内容就都需要等待。

如下图所示,我们一个用链条来表现一个连接上发送的两个流(传输),红色的与绿色的数据流:

这种单个数据包造成的阻塞,就是TCP上的队头阻塞(head of line blocking)。

随着丢包率的增加,HTTP/2的表现越来越差。在2%的丢包率(一个很差的网络质量)中,测试结果表明HTTP/1用户的性能更好,因为HTTP/1一般有六个TCP连接,哪怕其中一个连接阻塞了,其他没有丢包的连接仍然可以继续传输。
在限定的条件下,在TCP下解决这个问题相当困难。

  • QUIC怎么做的?

​ 使用QUIC时,两端间仍然建立一个连接,该连接也经过协商使得数据得到安全且可靠的传输。

但是,当我们在这个连接上建立两个不同的数据流时,它们互相独立。

也就是说,如果一个数据流丢包了,只有那个数据流必须停下来,等待重传。

下面是两个端点间的示意图,黄色与蓝色是两个独立的数据流。

减少延迟

与 TCP 的 3次 握手相比,QUIC 提供了 0-RTT 和 1-RTT 的握手,这减少了协商和建立新连接时所需的时间。

除此之外,QUIC提供了提早传输更多数据的“早期数据”(early data)特性,并且它的使用比TCP快速打开(TCP Fast Open)更加简便。

因为数据流概念的引入,客户端不用等待前一个连接结束,便可以与同一个主机建立另一个逻辑连接。


2、编译安装nginx-quic

安装前述

目前 nginx quic 主要有两种方式安装支持:

  1. 由 cloudflare 开源的 quiche 补丁 + boringssl 实现的。
  2. 由 nginx 官方 quic 分支 + boringssl 实现的。( 本文以此展开 )

这里我们使用了CentOS7来进行编译安装

[root@TINY-DESKTOP /root]# lsb_release -a
LSB Version:    :core-4.1-amd64:core-4.1-noarch:cxx-4.1-amd64:cxx-4.1-noarch:desktop-4.1-amd64:desktop-4.1-noarch:languages-4.1-amd64:languages-4.1-noarch:printing-4.1-amd64:printing-4.1-noarch
Distributor ID: CentOS
Description:    CentOS Linux release 7.8.2003 (Core)
Release:        7.8.2003
Codename:       Core

首先我们在系统中使用yum安装基本的编译工具,注意此次使用的很多软件工具和依赖都是需要较新版本的,因此后面会进行大量的编译安装,请确保机器拥有足够的性能(可能需要编译gcc)和良好的网络。主要需要使用到的工具软件的安装步骤如下:

2.1 yum安装依赖

需要注意的是这里安装所需要的软件很多,这里并不能全部列出来,但是除了列出来的软件对新版本有要求外,别的都可以直接使用yum进行安装,当然也可以自己根据需要编译安装新版本。

yum install "Development Tools"

需要注意的是网上有部分教程会让我们去安装build-essential,而这个套件在centos中是不存在的,我们直接安装Development Tools套件即可

2.2 编译安装gcc

CentOS7默认的gcc的版本太旧了,编译boringssl的时候会报错,我们手动编译安装新版本的gcc,这里使用的是10.2.0的版本。gcc的版本可以在这里下载http://ftp.gnu.org/gnu/gcc/

wget http://ftp.gnu.org/gnu/gcc/gcc-10.2.0/gcc-10.2.0.tar.gz
tar -zxvf gcc-10.2.0.tar.gz
cd gcc-10.2.0/

# 解压完成后需要下载四个依赖,我们执行脚本即可直接下载,服务器网络不好的同学也可以手动下载
./contrib/download_prerequisites

# 创建一个专门用来编译的目录
mkdir gcc-build-10.2.0
cd gcc-build-10.2.0/

# 这里需要对c和c++进行支持,为了节省时间禁用掉了交叉编译
../configure -enable-checking=release -enable-languages=c,c++ -disable-multilib
# 接下来这里需要很久
make -j8
make install

# 安装完成之后我们需要替换原来的cc文件和c++文件,确保它们的版本都是最新的版本
# 一般来说原来系统的cc文件和c++文件都在/usr/bin/目录下,而我们编译安装的cc文件和c++文件在/usr/local/bin/
cd /usr/bin/
mv /usr/bin/cc /usr/bin/cc.4.8.5
mv /usr/bin/c++ /usr/bin/c++.4.8.5
ln -s /usr/local/bin/gcc /usr/bin/cc
ln -s /usr/local/bin/c++ /usr/bin/c++

# 最后检查版本
/usr/bin/cc -v
/usr/bin/c++ -v

gcc下载的依赖内容如下,有需要的同学自取
[root@TINY-DESKTOP /home/gcc-10.2.0]# ./contrib/download_prerequisites
2020-08-21 21:28:24 URL:http://gcc.gnu.org/pub/gcc/infrastructure/gmp-6.1.0.tar.bz2 [2383840/2383840] -> “./gmp-6.1.0.tar.bz2” [1]
2020-08-21 21:28:26 URL:http://gcc.gnu.org/pub/gcc/infrastructure/mpfr-3.1.4.tar.bz2 [1279284/1279284] -> “./mpfr-3.1.4.tar.bz2” [1]
2020-08-21 21:28:28 URL:http://gcc.gnu.org/pub/gcc/infrastructure/mpc-1.0.3.tar.gz [669925/669925] -> “./mpc-1.0.3.tar.gz” [1]
2020-08-21 21:28:30 URL:http://gcc.gnu.org/pub/gcc/infrastructure/isl-0.18.tar.bz2 [1658291/1658291] -> “./isl-0.18.tar.bz2” [1]
gmp-6.1.0.tar.bz2: OK
mpfr-3.1.4.tar.bz2: OK
mpc-1.0.3.tar.gz: OK
isl-0.18.tar.bz2: OK
All prerequisites downloaded successfully.

2.3 编译安装perl

perl的安装包可以从这里www.cpan.org/src下载。perl的版本需要尽可能新,否则可能会出现问题,这里使用的是5.32.0的稳定版本(截止2020-08-21)

wget https://www.cpan.org/src/5.0/perl-5.32.0.tar.gz
tar -zxvf perl-5.32.0.tar.gz
cd perl-5.32.0/
./Configure -des -Dprefix=/usr/local/perl
make -j 8
make install

# 注意最后检查一下系统默认的perl是否为我们新安装的perl
perl -v

# 如果不是则需要进行修改
# 查看默认的perl
which perl | xargs file

# 替换新安装的perl和原来的perl
mv /usr/bin/perl /usr/bin/perl.5.16.3
# 需要注意新安装的perl目录要根据前面编译的时候指定的目录来确定
ln -s /usr/local/perl/bin/perl /usr/bin/perl
# 再次检查
perl -v

2.4 安装golang

golang的安装配置比较简单,我们从https://golang.org/dl直接下载最新的稳定版本即可。

wget https://golang.org/dl/go1.16.5.linux-amd64.tar.gz
tar -zxvf go1.16.5.linux-amd64.tar.gz -C /usr/local

# 修改系统默认的go文件
ln -s /usr/local/go/bin/go /usr/bin/go

mkdir /data/gopath

接下来的go环境变量同学们可以根据自己的实际需求进行配置。对于我个人而言,我直接在/etc/profile中添加下面的配置然后source生效即可。

export GOROOT=/usr/local/go
export GOBIN=$GOROOT/bin
export PATH=$PATH:$GOBIN
export GOPATH=/data/gopath

2.5 编译安装cmake

cmake的版本必须要在3.0以上,cmake可以到这里下载https://cmake.org/download/

wget https://github.com/Kitware/CMake/releases/download/v3.18.2/cmake-3.18.2.tar.gz
tar -zxvf cmake-3.18.2.tar.gz
cd cmake-3.18.2
./bootstrap
gmake
make
make install

# 检查版本
cmake --version
  • 可能遇到的问题 (centos6.5)
    • 升级完gcc之后仍可能遇到如下报错
      ---------------------------------------------
      gmake: `cmake' is up to date.
      /data/src_packages/cmake-3.18.2/Bootstrap.cmk/cmake: /usr/lib64/libstdc++.so.6: version `GLIBCXX_3.4.14' not found (required by /data/src_packages/cmake-3.18.2/Bootstrap.cmk/cmake)
      /data/src_packages/cmake-3.18.2/Bootstrap.cmk/cmake: /usr/lib64/libstdc++.so.6: version `GLIBCXX_3.4.18' not found (required by /data/src_packages/cmake-3.18.2/Bootstrap.cmk/cmake)
      /data/src_packages/cmake-3.18.2/Bootstrap.cmk/cmake: /usr/lib64/libstdc++.so.6: version `CXXABI_1.3.5' not found (required by /data/src_packages/cmake-3.18.2/Bootstrap.cmk/cmake)
      /data/src_packages/cmake-3.18.2/Bootstrap.cmk/cmake: /usr/lib64/libstdc++.so.6: version `GLIBCXX_3.4.26' not found (required by /data/src_packages/cmake-3.18.2/Bootstrap.cmk/cmake)
      /data/src_packages/cmake-3.18.2/Bootstrap.cmk/cmake: /usr/lib64/libstdc++.so.6: version `GLIBCXX_3.4.15' not found (required by /data/src_packages/cmake-3.18.2/Bootstrap.cmk/cmake)
      /data/src_packages/cmake-3.18.2/Bootstrap.cmk/cmake: /usr/lib64/libstdc++.so.6: version `GLIBCXX_3.4.20' not found (required by /data/src_packages/cmake-3.18.2/Bootstrap.cmk/cmake)
      /data/src_packages/cmake-3.18.2/Bootstrap.cmk/cmake: /usr/lib64/libstdc++.so.6: version `GLIBCXX_3.4.21' not found (required by /data/src_packages/cmake-3.18.2/Bootstrap.cmk/cmake)
       ---------------------------------------------
       Error when bootstrapping CMake:
       Problem while running initial CMake
    • 解决方式
    mv /usr/lib64/libstdc++.so.6 /usr/lib64/libstdc++.so.6.bak
    ln -s /usr/local/lib64/libstdc++.so.6.0.28 /usr/lib64/libstdc++.so.6
    
    # 再次验证是否能找到如上报错的GLIVCXX_3.4.21等
    strings /usr/lib64/libstdc++.so.6 | grep GLIBC

2.6 编译安装boringssl

谷歌官方建议我们使用ninja来编译安装boringssl,因此我们需要先安装一个ninja

cd /tmp/nginxsrc
wget https://github.com/ninja-build/ninja/releases/download/v1.10.1/ninja-linux.zip
unzip ninja-linux.zip
cp -r ninja /usr/bin/
which ninja
# 注意这里的git仓库很大,大概在250MB左右,请确保编译安装服务器的网络良好
git clone https://github.com/google/boringssl.git
cd boringssl/

# 建立一个专门用于编译的文件夹
mkdir build
cd build
cmake -GNinja ..
ninja

注意在执行cmake这一步的时候正常情况下检测到的gcc文件和perl库版本应该是我们之前编译安装好的新版本,如果不对的话需要再次检查
# cmake -GNinja ..
– The C compiler identification is GNU 10.2.0
– Detecting C compiler ABI info
– Detecting C compiler ABI info - done
– Check for working C compiler: /usr/bin/cc - skipped
– Detecting C compile features
– Detecting C compile features - done
– The CXX compiler identification is GNU 10.2.0
– Detecting CXX compiler ABI info
– Detecting CXX compiler ABI info - done
– Check for working CXX compiler: /usr/bin/c++ - skipped
– Detecting CXX compile features
– Detecting CXX compile features - done
– Found Perl: /usr/bin/perl (found version “5.32.0”)
– Checking for module ‘libunwind-generic’
– No package ‘libunwind-generic’ found
libunwind not found. Disabling unwind tests.
– The ASM compiler identification is GNU
– Found assembler: /usr/bin/cc
– Configuring done
– Generating done
– Build files have been written to: /root/boringssl/build

  • 可能遇到问题 (centos6.5)
    • 提示缺少GLIBC_2.15
      /usr/bin/ninja: /lib64/libc.so.6: version `GLIBC_2.15' not found (required by /usr/bin/ninja)

2.7 更新 lua-nginx-module 模块

  • 下载

    cd /tmp/nginxsrc
    wget https://github.com/openresty/lua-nginx-module/archive/refs/tags/v0.10.14.tar.gz
    
    wget https://github.com/openresty/luajit2/archive/refs/tags/v2.1-20210510.tar.gz
    
    wget https://github.com/vision5/ngx_devel_kit/archive/refs/tags/v0.3.1.tar.gz
    
    tar zxvf v0.10.14.tar.gz
    tar zxvf v2.1-20210510.tar.gz
    tar zxvf v0.3.1.tar.gz

2.8 编译nginx

剩下的nginx的编译安装步骤就和正常的nginx编译安装一致,这里不再赘述,模块可以根据自己的需求进行安装,开启HTTP/3模块需要使用

--with-http_v3_module

--with-cc-opt="-I../boringssl/include"

--with-ld-opt="-L../boringssl/build/ssl -L../boringssl/build/crypto"

作为核心参数。

  • 下载 nginx-quic ( 本篇下载时版本: 1.21.1 )

    cd /tmp/nginxsrc
    wget https://hg.nginx.org/nginx-quic/archive/quic.tar.gz
    tar -zxvf quic.tar.gz
  • 核心配置

    ./auto/configure --prefix=/usr/local/nginx --with-http_ssl_module --with-http_v2_module --with-http_v3_module --with-cc-opt="-I../boringssl/include" --with-ld-opt="-L../boringssl/build/ssl -L../boringssl/build/crypto"
  • 例如
    • 注意模块位置,例如lua、brotli、boringssl等
      cd nginx-quic-quic/
      
      export LUAJIT_LIB=/usr/local/luajit2.1/lib && export LUAJIT_INC=/usr/local/luajit2.1/include/luajit-2.1/
      
      ./auto/configure --prefix=/usr/local/nginx-1.18.0 --with-http_v2_module --with-http_realip_module --with-http_addition_module --with-pcre --with-http_sub_module --with-http_dav_module --with-http_flv_module --with-http_image_filter_module --with-http_mp4_module --with-http_ssl_module --with-http_gzip_static_module --with-http_random_index_module --with-http_secure_link_module --with-http_stub_status_module --with-http_auth_request_module --with-http_degradation_module --with-http_gunzip_module --with-file-aio --add-module=/tmp/nginxsrc/ngx_devel_kit-0.3.1 --add-module=/tmp/nginxsrc/lua-nginx-module-0.10.14 --add-module=/tmp/nginxsrc/audio-nginx-module --with-openssl=../boringssl --with-ld-opt=-Wl,-rpath,/usr/local/luajit2.1/lib --with-stream --add-module=/usr/local/nginx-module-vts-0.1.18 --add-module=/usr/local/src/ngx_brotli --with-stream_ssl_module --with-threads --with-http_v3_module --with-cc-opt=-I../boringssl/include --with-ld-opt='-L../boringssl/out/ssl -L../boringssl/out/crypto' --with-debug
      
      make 
      
      # 可选,如果已有版本在跑切勿使用!!
      make install

安装完成后我们检测nginx的参数:

# /usr/local/nginx-1.18.0/nginx -V
nginx version: nginx/1.21.1
built by gcc 10.2.0 (GCC)
built with OpenSSL 1.1.1 (compatible; BoringSSL) (running with BoringSSL)
TLS SNI support enabled
configure arguments: --prefix=/usr/local/nginx-1.18.0 --with-http_v2_module --with-http_realip_module --with-http_addition_module --with-pcre --with-http_sub_module --with-http_dav_module --with-http_flv_module --with-http_image_filter_module --with-http_mp4_module --with-http_ssl_module --with-http_gzip_static_module --with-http_random_index_module --with-http_secure_link_module --with-http_stub_status_module --with-http_auth_request_module --with-http_degradation_module --with-http_gunzip_module --with-file-aio --add-module=/tmp/nginxsrc/ngx_devel_kit-0.3.1 --add-module=/tmp/nginxsrc/lua-nginx-module-0.10.14 --add-module=/tmp/nginxsrc/audio-nginx-module --with-openssl=../boringssl --with-ld-opt=-Wl,-rpath,/usr/local/luajit2.1/lib --with-stream --add-module=/usr/local/nginx-module-vts-0.1.18 --add-module=/usr/local/src/ngx_brotli --with-stream_ssl_module --with-threads --with-http_v3_module --with-cc-opt=-I../boringssl/include --with-ld-opt='-L../boringssl/out/ssl -L../boringssl/out/crypto' --with-debug

2.9 平滑升级nginx

  • 由于本机不是首次安装,且有业务在跑,所以需要在 make 阶段完成后手动平滑升级。

  • 注意:

    • 对于已经在跑的 nginx 需要在线增加模块 make 完成后,不要执行 make install ,否则就会将原来数据覆盖!!
    • 具体 make install 会执行的操作可以详见源码目录下 objs/MAKEFILE 的 install 部分内容,约 2559 行;
    • 也不可使用 make upgrade,该方法也不会执行最终升级;
  • 大致流程如下

    • 复制新版二进制程序至 nginx 目录

      cp /tmp/nginxsrc/nginx-quic-quic/objs/nginx /usr/local/nginx/sbin/nginx-quic
    • 备份现有二进制,替换新的版本,并校验配置文件是否正常

      cd /usr/local/nginx/sbin/
      mv nginx nginx.bak
      mv nginx-quic nginx
      
      /usr/local/nginx/sbin/nginx -t -c
      /usr/local/nginx/conf/nginx.conf
    • 执行升级

      • kill -USR2 不让现有 worker 进程接受请求,当请求处理完就让 worker 进程退出

      • 同时也会启动新版nginx接收请求,所以此时会发现有两个master进程

        nginx_pid=`cat /usr/local/nginx/conf/nginx.pid`
        
        kill -USR2 $nginx_pid
        
        # (nginx.pid 中的内容即是旧的nginx主进程 pid,可通过 ps –ef |grep nginx 查看)

        如图:

      • 出现两个 master 新/旧 nginx 进程

      • 此时,我们还有机会可以决定使用新版还是恢复到旧版。

        不重载配置启动新/旧工作进程

        # 从容关闭旧/新进程
        kill -HUP 旧/新版主进程号
        
        # 如果此时报错,提示还有进程没有结束就用下面命令先关闭旧/新工作进程,再关闭主进程号
        kill -QUIT 旧/新主进程号
        
        kill -TERM 旧/新工作进程号

        注意,如果操作不当变成直接 kill 了就会全部连接失败,生产最好下掉前端流量再做升级

      • 如果无需回滚,则继续执行下面命令,完全把旧的进程退出,启用新的进程,如下:

        # 处理完关闭旧的worker进程
        # kill -WINCH ${旧的主PID}
        kill -WINCH $nginx_pid
        
        # 关闭旧的主进程
        # kill -QUIT ${旧的主PID}
        kill -QUIT $nginx_pid
  • 就此完成平滑升级。

  • 正常操作,过程平滑,QPS1000请求并未波动
  • 且中途如有reload,也不会影响正常请求
  • 小结
    • 如果要恢复到旧版本,只需要上面的几个步骤都是操作新版主进程号
    • 如果要用新版本,就上面的几个步骤都操作旧版主进程号就行

3、配置nginx-quic

特性说明

  • 由于HTTP/3需要使用udp协议端口,请注意开放对应的防火墙

  • http2监听的是443的tcp端口,而http3监听的是udp端口

  • nginx中添加了$http3$quic变量,可以添加到日志中,这样就可以看到是否使用了HTTP/3来进行访问了

  • 如果有多个server_name,在不指定IP的情况下,只需要在任意一个配置了listen 443 http3 quic reuseport;那么其他所有server_name都会开启HTTP3,并且不需要再添加该配置否则会报错(不知道后续会不会把报错去掉),如果需要部分server_name开启HTTP/3,请指定监听IP。

  • http3增加了

    • http3_max_field_size
    • http3_max_table_capacity
    • http3_max_blocked_streams
    • http3_max_concurrent_pushes
    • http3_push
    • http3_push_preload
    • 这六个变量来控制http3的性能
  • 替代服务(alternative service, Alt-svc:)头部和它相对应的 ALT-SVC HTTP/2帧并不是特别为QUIC和HTTP/3设计的。它是为了让服务器可以告诉客户端 “看,我在这个主机的这个端口用这个协议提供相同的服务” 而设计的,详见RFC 7838
    如果初始连接使用的是HTTP/2(甚至HTTP/1),服务器可以响应并告诉客户端它可以再试试HTTP/3。连接可以指向相同主机或者不同但提供相同服务的主机。Alt-svc回复中有一个到期计时器,让客户端可以在指定的时间内使用建议的替代协议将后续的连接和请求直接发送给替代主机。

    • 例子

      一个HTTP服务器的响应中包含了如下的一个 Alt-Svc: 头部:
      Alt-Svc: h3=":50781"

      这指示了同一名称的主机在UDP端口50781提供HTTP/3服务。

      然后,客户端可以尝试与该端口建立QUIC连接。如果成功,后续将通过该连接继续通信,代替初始的HTTP版本。

  • 具体配置

server {
    listen 443 ssl http2;
    listen 443 http3 quic reuseport;
    listen 8443 quic ; # 重复 quic 端口只要声明quic即可

    # UDP listener for QUIC+HTTP/3

    server_name nestealin.com draft.nestealin.com;

    ssl_certificate      /data/keys/server.cer;
    ssl_certificate_key  /data/keys/server.key;  
    ssl_session_cache shared:SSL:30m;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ecdh_curve X25519:P-256:P-384;
    ssl_ciphers TLS13-CHACHA20-POLY1305-SHA256:TLS13-AES-256-GCM-SHA384:TLS13-AES-128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-CHACHA20-POLY1305:EECDH+CHACHA20:EECDH+AES128;

    # 该选项用于开启address validation,但是会和0-RTT冲突
    #quic_retry on;

    # 开启 TLS 1.3 0-RTT
    ssl_early_data on;
    # 添加 Early-Data 头告知后端, 防止重放攻击
    proxy_set_header Early-Data $ssl_early_data;

    # 替代服务头部,告诉客户端,服务端的443端口用这个协议提供相同的服务
    # 参考nginx官方目前支持的http3版本,我们添加对应的header
    add_header Alt-Svc 'h3-29=":8443"; ma=2592000,h3-T051=":8443"; ma=2592000,h3-Q050=":8443"; ma=2592000,h3-Q046=":8443"; ma=2592000,h3-Q043=":8443"; ma=2592000,quic=":8443"; ma=2592000; v="46,43"';  # 兼容不支持QUIC的浏览器

    add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload";

  ......
         
 }

4、测试

目前的主流浏览器只有Firefox和Chrome支持了HTTP/3协议。并且两者支持程度并不高,Firefox是默认禁用,Chrome是默认开启部分,因此配置起来有点麻烦。

  • 单独测试TLS1.3
    /usr/local/openssl111/bin/openssl  s_client -connect draft.nestealin.com:443  -tls1_3

4.1 HTTP/3 CHECK

https://www.http3check.net 网站提供了网站的HTTP/3支持检测

4.2 Firefox开启HTTP/3

对于firefox要求版本在75+,我们使用 90.0.2 版本进行测试:

在firefox中开启http3比较简单,我们直接在地址栏中输入 about:config ,然后搜索 http3 ,将 network.http.http3.enabled 设置为 true ,接着重启浏览器。

再次访问页面就可以看到使用了HTTP/3协议进行访问,在后台的日志中也能看到对应的请求使用了HTTP3quic

192.168.1.12 - - [12/Aug/2021:00:47:31 +0800] "GET / HTTP/3.0" 200 7078 "https://draft.nestealin.com:8443/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:89.0) Gecko/20100101 Firefox/89.0" "quic"

4.3 Chrome开启HTTP/3

nginx的官网提示说Chrome需要83+版本才支持HTTP/3,实测在部分网站(如google.com)确实可以使用HTTP/3的某些草案版本,但是Chrome目前来说默认还是不支持h3-27协议,我们需要手动开启。对于windows系统而言,我们使用命令行+指定参数的方式开启h3-27的支持:

PS C:\Program Files (x86)\Google\Chrome\Application> .\chrome.exe --enable-quic --quic-version=h3-27
PS C:\Program Files (x86)\Google\Chrome\Application> .\chrome.exe --enable-quic --quic-version=h3-27 --origin-to-force-quic-on=tinychen.com:443

上面使用了powershell来启动chrome,为了保证效果,还使用了--origin-to-force-quic-on来强行指定域名和端口。

本次测试的chrome版本如下:

测试效果如下:

除了本次的测试网站,我们还可以看到谷歌的大部分网站也都开启了HTTP/3。

同样的我们在后台日志中也能看到对应的访问日志。

192.168.1.12 - - [12/Aug/2021:00:50:38 +0800] "GET / HTTP/3.0" 200 7078 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.131 Safari/537.36" "quic"

4.4 终端方式确认

Mac

# 注意 Mac 自带的和这个是共存的,使用时需要调用当前安装的
brew install --HEAD -s https://raw.githubusercontent.com/cloudflare/homebrew-cloudflare/master/curl.rb
curl -I  --http3 https://www.nange.cn/
 
# 测试结果
--- ~ » curl -I --http3 https://www.nange.cn
HTTP/3 200
server: NAN
date: Thu, 19 Mar 2020 09:58:04 GMT
content-type: text/html; charset=UTF-8
vary: Accept-Encoding
last-modified: Thu, 19 Mar 2020 07:49:24 GMT
strict-transport-security: max-age=63072000; includeSubDomains; preload
access-control-allow-origin: *
alt-svc: quic=":443"; ma=2592000

Linux

# 可以参考 https://github.com/curl/curl/blob/master/docs/HTTP3.md
git clone --recursive https://github.com/cloudflare/quiche
 
cd quiche/deps/boringssl
mkdir build
cd build
cmake -DCMAKE_POSITION_INDEPENDENT_CODE=on ..
make
cd ..
mkdir -p .openssl/lib
cp build/crypto/libcrypto.a build/ssl/libssl.a .openssl/lib
ln -s $PWD/include .openssl
 
cd ../..
QUICHE_BSSL_PATH=$PWD/deps/boringssl cargo build --release --features pkg-config-meta
 
cd ..
git clone https://github.com/curl/curl
cd curl
./buildconf
./configure LDFLAGS="-Wl,-rpath,$PWD/../quiche/target/release" --with-ssl=$PWD/../quiche/deps/boringssl/.openssl --with-quiche=$PWD/../quiche/target/release
make
# 测试结果
[root@Her ~]# curl -I --http3 https://www.nange.cn
HTTP/3 200
server: NAN
date: Thu, 19 Mar 2020 10:01:06 GMT
content-type: text/html; charset=UTF-8
vary: Accept-Encoding
last-modified: Thu, 19 Mar 2020 07:49:24 GMT
strict-transport-security: max-age=63072000; includeSubDomains; preload
access-control-allow-origin: *
alt-svc: quic=":443"; ma=2592000

5、遇到的问题

  • 加入 lua-nginx-module 模块在编译时报错

    官方仓库

    https://github.com/openresty/lua-nginx-module#table-of-contents

    大致原因,模块尚未支持quic分支的nginx版本,所以本次编译剔除下面两个参数

    --add-module=/tmp/nginxsrc/lua-nginx-module-0.10.9 
    --with-ld-opt=-Wl,-rpath,/usr/local/luajit/lib

    已解决,详见编译安装步骤 2.7 更新 lua-nginx-module 模块

    • 原因如下

      • 由于当前 nginx 已经在用 lua 相关模块,在安装期间发现老版本模块会编译不过,导致如下报错

        make[1]: *** [objs/addon/src/ngx_http_lua_script.o] Error 1
        make[1]: *** Waiting for unfinished jobs....
        /tmp/nginxsrc/lua-nginx-module-0.10.9/src/ngx_http_lua_module.c: In function ‘ngx_http_lua_merge_srv_conf’:
        /tmp/nginxsrc/lua-nginx-module-0.10.9/src/ngx_http_lua_module.c:1022:37: error: passing argument 2 of ‘SSL_CTX_sess_set_get_cb’ from incompatible pointer type [-Werror=incompatible-pointer-types]
         1022 |                                     ngx_http_lua_ssl_sess_fetch_handler);
              |                                     ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
              |                                     |
              |                                     SSL_SESSION * (*)(SSL *, u_char *, int,  int *) {aka struct ssl_session_st * (*)(struct ssl_st *, unsigned char *, int,  int *)}
        In file included from src/event/ngx_event_openssl.h:15,
                         from src/core/ngx_core.h:85,
                         from /tmp/nginxsrc/lua-nginx-module-0.10.9/src/ddebug.h:13,
                         from /tmp/nginxsrc/lua-nginx-module-0.10.9/src/ngx_http_lua_module.c:11:
        ../boringssl/include/openssl/ssl.h:2085:34: note: expected ‘SSL_SESSION * (*)(SSL *, const uint8_t *, int,  int *){aka ‘struct ssl_session_st * (*)(struct ssl_st *, const unsigned char *, int,  int *)} but argument is of type ‘SSL_SESSION * (*)(SSL *, u_char *, int,  int *){aka ‘struct ssl_session_st * (*)(struct ssl_st *, unsigned char *, int,  int *)}
         2085 |     SSL_CTX *ctx, SSL_SESSION *(*get_session_cb)(SSL *ssl, const uint8_t *id,
              |                   ~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
         2086 |                                                  int id_len, int *out_copy));
              |                                                  ~~~~~~~~~~~~~~~~~~~~~~~~~~
        cc1: all warnings being treated as errors
        make[1]: *** [objs/addon/src/ngx_http_lua_module.o] Error 1
        make[1]: Leaving directory `/data/install_pkg/nginx-quic-quic'
        make: *** [build] Error 2

        原则上,秉持尽可能不做大变化的前提下测试,结果是可以维持现有的luajit版本,只升级 lua-nginx-module 至 0.10.14 版本即可。

        • 实测不升级 luajit 也能用,但 nginx 启动时会有如下提醒:
        nginx: [alert] detected a LuaJIT version which is not OpenResty's; many optimizations will be disabled and performance will be compromised (see https://github.com/openresty/luajit2 for OpenResty's LuaJIT or, even better, consider using the OpenResty releases from https://openresty.org/en/download.html)

        所以本次索性升级 lua-nginx-module-0.10.14 + luajit2-2.1 + ngx-devel-kit-0.3.1

  • 使用新版本 lua-nginx-module 模块编译后,log_by_lua 功能出现报错

    • 先后尝试

      • 更换 lua-nginx-module 版本从 10-13 均无法通过编译,无果
      • 只升级 luajit 至 2.1 ,也无果
      • 升级 ngx-devel-kit 至 3.1, 也无果
    • 当前配置如下

      log_by_lua '
        metric_requests:inc(1, {ngx.var.server_name, ngx.var.status})
        metric_latency:observe(tonumber(ngx.var.request_time), {ngx.var.server_name})
      ';
    • 报错信息如下

      2021/08/11 12:55:50 [debug] 2178691#2178691: *132 lua log handler, uri:"/favicon.ico" c:0
      2021/08/11 12:55:50 [debug] 2178691#2178691: *132 looking up Lua code cache with key '=log_by_lua(nginx.conf:154)nhli_dd0f260c106be91cae0f707933161e93'
      2021/08/11 12:55:50 [error] 2178691#2178691: *132 failed to run log_by_lua*: log_by_lua(nginx.conf:154):2: API disabled in the current context
      stack traceback:
      	[C]: in function '__index'
      	log_by_lua(nginx.conf:154):2: in function <log_by_lua(nginx.conf:154):1> while logging request, client: 192.168.1.13, server: draft.nestealin.com, request: "GET /favicon.ico HTTP/3.0", referrer: "https://draft.nestealin.com:8443/"

    暂无解决方式

  • 通过 HTTP3.0 的访问,无法使用 $http_host 变量

    • 因原访问日志输出格式为

      access_log  logs/$http_host.access.main.log  main;
    • 而在 HTTP3 的请求时,无法命中该变量,导致请求日志输出到 logs/.access.main.log 文件中( 目录下 ll 命令看不到该文件,需要使用 ls -aFl ),通过 debug 模式可以看出输出方式

      2021/08/11 12:55:50 [debug] 2178691#2178691: *132 http log handler
      2021/08/11 12:55:50 [debug] 2178691#2178691: *132 http map started
      2021/08/11 12:55:50 [debug] 2178691#2178691: *132 http script var: "192.168.1.13"
      2021/08/11 12:55:50 [debug] 2178691#2178691: *132 http map: "" "192.168.1.13"
      2021/08/11 12:55:50 [debug] 2178691#2178691: *132 http script copy: "/usr/local/nginx-1.18.0/logs/"
      2021/08/11 12:55:50 [debug] 2178691#2178691: *132 http script copy: ".access.main.log"
      2021/08/11 12:55:50 [debug] 2178691#2178691: *132 http log "/usr/local/nginx-1.18.0/logs/.access.main.log"
      2021/08/11 12:55:50 [debug] 2178691#2178691: *132 add cleanup: 0000000001ADE288
  • quic 是基于UDP协议的,所以需要放行对应的UDP端口

    # 以 CentOS 7.7 为例
    # 先查看下以开放的端口,如果有就不必开启了
    firewall-cmd --zone=public --list-ports
    # 没有的话,开启相应协议端口,其它端口亦是如此。
    # 开启 80 udp端口
    firewall-cmd --zone=public --add-port=80/udp --permanent
    # 开启 443 udp 端口
    firewall-cmd --zone=public --add-port=443/udp --permanent
    # 重启防火墙服务
    systemctl restart firewalld.service

可能出错一


make -f objs/Makefile
make[1]: 进入目录“/www/server/nginx/nginx-1.19.4”
mkdir -p ../quiche/deps/boringssl/build ../quiche/deps/boringssl/.openssl/lib ../quiche/deps/boringssl/.openssl/include/openssl \
&& cd ../quiche/deps/boringssl/build \
&& cmake -DCMAKE_C_FLAGS="" -DCMAKE_CXX_FLAGS="" .. \
&& make VERBOSE=1 \
&& cd .. \
&& cp -r include/openssl/*.h .openssl/include/openssl \
&& cp build/ssl/libssl.a build/crypto/libcrypto.a .openssl/lib
CMake Error at CMakeLists.txt:1 (cmake_minimum_required):
  CMake 3.0 or higher is required.  You are running version 2.8.12.2
 
 
-- Configuring incomplete, errors occurred!
make[1]: *** [../quiche/deps/boringssl/.openssl/include/openssl/ssl.h] 错误 1
make[1]: 离开目录“/www/server/nginx/nginx-1.19.4”
make: *** [build] 错误 2

  • 解决:
# cmake 版本太低,安装高于 3.0 的版本即可。然后执行
touch ../quiche/deps/boringssl/.openssl/include/openssl/ssl.h

可能出错二

...
--   No package 'libunwind-generic' found
libunwind not found. Disabling unwind tests.
CMake Error at CMakeLists.txt:51 (message):
  Could not find Go
...
  • 解决:

# go 没有安装好,缺少需要的扩展库。执行
yum install libunwind-devel

可能出错三

...
make -f objs/Makefile
make[1]: 进入目录“/www/server/nginx/nginx-1.19.4”
cd ../quiche && cargo build --release --no-default-features
/bin/sh: cargo: 未找到命令
make[1]: *** [../quiche/target/release/libquiche.a] 错误 127
make[1]: 离开目录“/www/server/nginx/nginx-1.19.4”
make: *** [build] 错误 2
  • 解决
# 没有装好rust 或环境变量没有配置对,重新安装或配置。具体前边的准备工作有提到。

可能出错四

...
/www/server/nginx/src/lua_nginx_module/src/ngx_http_lua_script.c: In function ‘ngx_http_lua_script_add_copy_code’:
/www/server/nginx/src/lua_nginx_module/src/ngx_http_lua_script.c:332:18: error: cast between incompatible function types from ‘size_t (*)(ngx_http_lua_script_engine_t *){aka ‘long unsigned int (*)(struct <anonymous> *)} to ‘void (*)(ngx_http_lua_script_engine_t *){aka ‘void (*)(struct <anonymous> *)} [-Werror=cast-function-type]
     code->code = (ngx_http_lua_script_code_pt)
                  ^
/www/server/nginx/src/lua_nginx_module/src/ngx_http_lua_script.c: In function ‘ngx_http_lua_script_add_capture_code’:
/www/server/nginx/src/lua_nginx_module/src/ngx_http_lua_script.c:402:18: error: cast between incompatible function types from ‘size_t (*)(ngx_http_lua_script_engine_t *){aka ‘long unsigned int (*)(struct <anonymous> *)} to ‘void (*)(ngx_http_lua_script_engine_t *){aka ‘void (*)(struct <anonymous> *)} [-Werror=cast-function-type]
     code->code = (ngx_http_lua_script_code_pt)
                  ^
cc1: all warnings being treated as errors
make[1]: *** [objs/Makefile:1912: objs/addon/src/ngx_http_lua_script.o] Error 1
make[1]: *** Waiting for unfinished jobs....
make[1]: Leaving directory '/www/server/nginx/src'
make: *** [Makefile:8: build] Error 2
  • 解决
# 修改 ../nginx/src/objs/Makefile 文件,删除第三行的 -Werror 参数,保存
...
CC =	cc
CFLAGS =   -pipe  -O -W -Wall -Wpointer-arith -Wno-unused-parameter -Werror -g  -DNDK_SET_VAR  -D_GLIBCXX_USE_CXX11_ABI=0 -Wno-deprecated-declarations
CPP =	cc -E
LINK =	$(CC)
...
 
# 然后执行
touch ../quiche/deps/boringssl/.openssl/include/openssl/ssl.h
 
# 再重新执行
make -j$(nproc --all) 

可能出错五

...
[ 84%] Building CXX object ssl/CMakeFiles/ssl.dir/tls13_client.cc.o
cd /www/server/nginx/quiche/deps/boringssl/build/ssl && /usr/bin/c++  -DBORINGSSL_DISPATCH_TEST -DBORINGSSL_HAVE_LIBUNWIND -DBORINGSSL_IMPLEMENTATION -I/www/server/nginx/quiche/deps/boringssl/third_party/googletest/include -I/www/server/nginx/quiche/deps/boringssl/ssl/../include  -Werror -Wformat=2 -Wsign-compare -Wmissing-field-initializers -Wwrite-strings -Wvla -ggdb -Wall -fvisibility=hidden -fno-common -Wno-free-nonheap-object -Wmissing-declarations -std=c++11 -fno-exceptions -fno-rtti -Wshadow   -o CMakeFiles/ssl.dir/tls13_client.cc.o -c /www/server/nginx/quiche/deps/boringssl/ssl/tls13_client.cc
/www/server/nginx/quiche/deps/boringssl/crypto/trust_token/trust_token_test.cc: In constructor ‘bssl::{anonymous}::TrustTokenProtocolTestBase::TrustTokenProtocolTestBase(const TRUST_TOKEN_METHOD*)’:
/www/server/nginx/quiche/deps/boringssl/crypto/trust_token/trust_token_test.cc:100:7: error: declaration of ‘method’ shadows a member of 'this' [-Werror=shadow]
       : method_(method) {}
       ^
/www/server/nginx/quiche/deps/boringssl/crypto/trust_token/trust_token_test.cc: In member function ‘virtual void bssl::{anonymous}::TrustTokenMetadataTest_TruncatedProof_Test::TestBody()’:
/www/server/nginx/quiche/deps/boringssl/crypto/trust_token/trust_token_test.cc:589:12: error: declaration of ‘public_metadata’ shadows a member of 'this' [-Werror=shadow]
   uint32_t public_metadata;
            ^
/www/server/nginx/quiche/deps/boringssl/crypto/trust_token/trust_token_test.cc: In member function ‘virtual void bssl::{anonymous}::TrustTokenMetadataTest_ExcessDataProof_Test::TestBody()’:
/www/server/nginx/quiche/deps/boringssl/crypto/trust_token/trust_token_test.cc:657:12: error: declaration of ‘public_metadata’ shadows a member of 'this' [-Werror=shadow]
   uint32_t public_metadata;
...
  • 解决

解决方式同出错四。


参考链接


文章作者: NesTeaLin
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 NesTeaLin !
  目录