最近在折腾转发tcp请求,原本我是用的是HAproxy,换了服务器之后本来想继续使用HAproxy的,不知怎么的在阴差阳错之下查到了nginx(版本号在1.11之后的)也支持tcp、udp请求的转发和负载均衡了,本着无限折腾的精神,我就上手了nginx。本文由以下几个部分组成:
本文使用的系统是ubuntu16.04,其他linux系统具体的操作请参阅linux下安装nginx
ubuntu下nginx最新版的安装
tcp转发与负载均衡的配置
根据请求域名转发tcp之我的尝试与屈服
一、ubuntu下的nginx最新安装
nginx现在最新的stable的版本号是1.12.2,如果偷懒直接在ubuntu下面使用
apt-get install nginx
进行安装,很不幸,安装出来的版本是1.10.*,差两个大版本啊。所以只能安装官方给的指导安装啦。
1.1 为ubuntu增加源
1.1.1 源内容
Replace $release with your corresponding Ubuntu release.
deb http://nginx.org/packages/ubuntu/ $release nginx
deb-src http://nginx.org/packages/ubuntu/ $release nginx
请注意这里面的$release是变量,具体的值根据不同的ubuntu版本来填写
ubuntu版本 $release
16.04 xenial
14.04 trusty
12.04 precise
17.04 zesty
所以我们比较常用的16.04的源如下:
deb http://nginx.org/packages/ubuntu/ xenial nginx
deb-src http://nginx.org/packages/ubuntu/ xenial nginx
1.1.2 增加源
这里有两种办法
直接在/etc/apt/sources.list后面加
在/etc/apt/sources.list.d这个文件夹里面新建一个list文件,比如新建nginx.list
我在这里选择第二种,新建nginx.list,在里面粘贴16.04的源的内容
1.2 开始安装
执行以下bash命令
sudo apt-get update
sudo apt-get install nginx
如果报错
W: GPG error: http://nginx.org/packages/ubuntu xenial Release: The following signatures couldn’t be verified because the public key is not available: NO_PUBKEY ABF5BXXXXXD9BF6
注意报错的最后,是PUBKEY的值,后续的操作需要使用到这个值,我们把这个值使用$key来表示。例如,$key=ABF5BQWEDSD9BF6。有了这个值之后我们执行以下的命令:
使用你的NO_PUBKEY的值来替换掉这里的$key,例如:ABF5BQWEDSD9BF6
sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys $key
sudo apt-get update
sudo apt-get install nginx
安装完成了之后可以运行,有可能需要执行两遍apt-key的命令,我安装的时候就是执行了这个命令两次,有两个不同的$key的值。
nginx -v
1
来查看nginx的版本,不出意外因该是1.12.2
二、tcp转发与负载均衡的配置
2.1 tcp转发的配置
nginx的tcp转发比较简单,具体的配置文件如下:
stream {
server {
## ipv6监听80端口 listen [::1]:80;
listen 2018; ##这个没有具体测试,不知道是否能够监听ipv6,ipv4肯定能够监听
proxy_pass nas.mm**.win:2020;##把请求转发至nas.mm**.win这个服务器
}
stream这个块在nginx的配置文件的顶级块里面,和原来http的块同属一个层级,如果需要监听udp的端口,在listen 2018的后面添加udp,具体如下:
server {
## 监听udp端口
listen 2019 udp;
proxy_pass *.com;
}
其他的高级设置参见nginx tcp、udp转发的其他设置
2.2 tcp负载均衡的配置
负载均衡的设置也是非常简单的,值是把原来的proxy_pass由一个服务器转移到一整个服务器组。具体的配置如下:
stream{
upstream nas_group {
#可以根据hash值来选择去某个服务器
server 127.0.0.1:28000;#这里还可以配置weight
server 127.0.0.1:28001;
}
server {
listen nas.****.win:80;
proxy_pass nas_group;
}
}
更加详细的配置请参考nginx tcp负载均衡配置
三、nginx根据请求域名转发tcp请求之我的尝试与屈服
我捣鼓nginx的目的不是为了做tcp的负载均衡,对于一个个人的玩家来说,我也没有那么大的并发需求,需要用得到负载均衡。我的主要目的是实现监听某个端口,例如80端口,当不同的请求从不同的域名上面来的时候,我们分配到不同的后端服务器去处理。
这个目标在nginx做http请求转发的时候很容易就实现出来,直接在server里面增加server_name这个字段,指定域名的访问。但是我的目标是转发tcp请求也实现一样的效果。于是我开始各种资料查询,stackoverflow、官方文档、知乎。。。
3.1 摸索过程
3.1.1 增加server_name字段
这个很容易想到,nginx转发http的时候,在server里面增加server_name字段,就可以实现监听同一个端口,不同域名请求转发到不同的后端服务器。所以我在转发tcp请求的时候也增加server_name字段,结果可想而知,配置文件错误,nginx没法运行起来。
3.1.2 通过使用map这种相对而言比较高级的东东
在server_name的失败尝试之后,我在stackoverflow上面看到了跟我一样需求的人的问题,别人下面给出了解决方案。
这个问题的地址如下:
https://stackoverflow.com/questions/34741571/nginx-tcp-forwarding-based-on-hostname/44821204#44821204
第一个回答者告诉提问者,是做不到的,然后给出了一堆堆东西。但是后面两个回答者给出了非常具有参考价值的回答,具体内容如下:
stream {
upstream pod53{
server 10.1.5.3:3306;
}
upstream pod54{
server 10.1.5.4:3306;
}
map $server_addr $x {
192.168.168.238 pod53;
192.168.168.239 pod54;
}
server {
listen 3306;
proxy_pass $x;
}
}
一看到这个答案,很容就可以理解map,map 后面是两个变量,经查询文档得知第一个变量一般是系统内置的变量,第二个变量应该是我们可以随便起名字然后使用的变量,楼主在这个地方使用了$server_addr的这个系统变量,这个变量是指当前服务器的ip地址,这里有两个ip地址,所以可以猜测到这个服务器有两个ip地址,这两个ip地址都是指到这个服务器的。这里实现了不同的服务器ip地址转发到不同的服务器上面。
这个答案就与我需要的不同的域名到不同的后端服务器非常相近了,这里是不同的服务器地址,于是我就去查询文档,希望看到更多的内置变量,拿到更多的数据。经过一番查找之后,我找到了stream这个块下面的所有内置变量,具体的请参阅http://nginx.org/en/docs/stream/ngx_stream_core_module.html ,在这个里面我找到了几个可能跟我们需求相关的变量:
变量名 解释
hostname 主机名称
remote_addr 远程地址
server_addr 服务器地址
很高兴我看到了hostname这个变量,这不就是域名么,于是我就开始尝试了,我把配置文件修改如下:
map $hostname $x {
nas.****.win pod53;
vps.****.win pod54;
}
修改完了之后就开始高高兴兴的测试,发现了nginx没有报错,正常跑起来了,可是测试请求转发的时候发现请求没有转发成功。于是我就去检查日志,看到了日志里面是这样的:
2018/03/16 05:58:45 [info] 31767#31767: *66 client 123.179.5.121:8386 connected to 0.0.0.0:80
2018/03/16 05:58:45 [error] 31767#31767: *66 no host in upstream "", client: 123.179.5.121, server: 0.0.0.0:80, bytes from/to client:0/0, bytes from/to upstream:0/0
这个里面不断提示no host in upstream,这个时候我就慢慢明白了自己的错误。
3.2 释怀与理解
为啥在转发http时候有server_name这个参数,为啥在转发tcp时候没有这个参数,为啥获取tcp的hostname获取不到,为啥server_addr又是可以拿得到的?这个跟http和tcp本身有关,tcp在7层网络结构的第4层,而http在网络结构的第7层。下面我们仔细分析一下:
3.2.1 http报文
下面我们来看一下http的典型报文结构:
报文的结构比较复杂,一般我们使用到的字段不定,所以报文头的长度也是不定的,下面我们看一下具体的实际的报文内容:
GET /biyeymyhjob/archive/2012/07/28/2612910.html HTTP/1.1
Host: www.cnblogs.com
Connection: keep-alive
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.162 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Referer: https://www.google.com/
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
If-Modified-Since: Fri, 16 Mar 2018 06:36:28 GMT
这是一段真正的http报文头,里面有一个很重要的头部字段Host,这个字段说明了我们请求的是什么域名,所以nginx可以根据这个字段来分析我们需要被转发到哪个具体的后端服务器。完整的http协议的解释参见http报文详解。
3.2.2 tcp报文
看完了应用层的报文,我们再看看传输层的tcp报文
这个里面最重要的就是端口号到端口的,压根就没有host之类的字段,那么只有端口,数据是怎么传递的呢?这个需要我们再往前面看一层,也就是网络结构的第三层:
由上面这两张图片,我们就很清楚了,tcp的整个报文封装在ip报文的数据部分,ip报文头里面有详细的源ip,目的ip,所以我们拿得到tcp报文的时候是可以查询得到ip请求从哪里来,当前服务器的ip地址。但是,我们完全不知道当前请求的服务器ip对应的域名是多少,这个东西只在http报文里面封装过,但是我们是tcp请求的转发,所以拿不到域名信息。所以我们想根据域名来转发tcp请求的目标落空了,但是根据服务器ip来转发还是可以实现的。
四、结束语
虽然最后我想做的没有成功,但是我还是很高兴,这次摸索又学到了不少的知识