rsync 工具备份服务端配置

在服务器端安装和配置rsync

1.安装rsync

如已默认安装,请卸载旧版本

$ sudo yum remove rsync -y

RPM 安装

RPM方式的好处,快速、方便、节时,具体安装如下:

$ yum -y install rsync

rsync文件:

/etc/rsyncd.conf
/etc/sysconfig/rsyncd
/etc/xinetd.d/rsync
/usr/bin/rsync
/usr/share/doc/rsync-3.1.2/COPYING
......

2.关于rsync认证方式

rsync有2种常用的认证方式,一种是rsync-daemon,另一种是SSH。

在生产环境中,通常使用rsync-daemon认证模式。

认证模式说明:

  • rsync-daemon认证:默认监听TCP的873端口。前提是双方都需要安装rsync,客户端可不启动rsync服务,但需要简单的配置。服务器端需要启动且需在服务器端配置rsync。

  • SSH认证:通过系统用户认证,即在rsync上通过SSH隧道进行传输,前提是需要服务器端与客户端建立无密码登录。

    • 无需服务器与客户端配置rsync,也无需启动rsync服务,只需双方都安装rsync即可。

3.配置服务端

3.1 设置rsync服务端密码文件 192.168.0.2

使用rsync-daemon认证方式。创建访问密码,格式为用户名:密码,一行一个,明文。
命令

$ sudo echo "renwole:renwolecom"  >>/etc/rsync.password
$ sudo chmod 600 /etc/rsync.password                    # 注意权限必须是 600 否则会报错

3.2 配置rsync服务端配置文件 192.168.0.2

配置文件所在目录: /etc/rsyncd.conf

$ sudo vim /etc/rsyncd.conf
uid = root               # 运行RSYNC守护进程的用户
gid = root               # 运行RSYNC守护进程的组
port = 873               # 默认端口
#address = 10.28.204.65  # 服务器IP地址
# pid file = /var/run/rsyncd.pid    # 进程启动后,进程号存放路径     centos 7 若不注释会报错
lock file = /var/run/rsync.lock   # 设置锁文件名称
log file = /var/log/rsyncd.log      # 指定rsync的日志文件
                                    # 这两个文件在配置过后系统会自己创建
use chroot = no             # 不使用chroot
read only = yes             # 只读,不让客户端上传文件到服务器
transfer logging = yes      # 将传输操作记录到传输日志文件

hosts allow=192.168.0.3             # 允许哪些主机访问(多个以空格隔开)
hosts deny=*                        # 拒绝哪些主机访问
max connections = 3                 # 最大连接数
# motd file = /etc/rsyncd.motd      # 登陆欢迎信息(生产环境不建议)

log format = %t %a %m %f %b        # 指定日志记录的格式
syslog facility = local3           # 消息级别
timeout = 600                      # 会话超时时间。

[BackupServer]              # 模块的名称,可以自定义    后续客户端进行同步的时候会使用到
path = /xxx/xxx             # 需要同步的目录
list=yes                    # 是否允许用户列出文件,默认为true
ignore errors               # 忽略错误信息
# exclude = myrenwole/      # 不同步的目录(多个以空格隔开)
comment = BackupServer      # 注释内容,任意
auth users = backup         # 那些用户才允许连接该模块,多个以,隔开     此用户是你上一步配置密码文件所设置的用户
secrets file = /etc/rsyncs.password    # 认证时所需的密码文件

注意:全局配置中的选项对所有模块有效;模块下定义的仅对当前模块有效;另外,模块中定义选项值优先于全局配置。

4.设置防火墙

$ sudo firewall-cmd --add-port=873/tcp --permanent
$ sudo firewall-cmd --add-port=873/udp --permanent
$ sudo firewall-cmd --reload

5.启动并加入开机自启动

$ sudo systemctl start rsyncd       # 开启
$ sudo systemctl enable rsyncd      # 开启守护进程
$ sudo systemctl list-unit-files    # 查看系统所有服务的状态

rsync的小坑

今天磁盘满了,打算将占磁盘大的文件给移走,再采用软连接的。同步的时候出现了一点小问题。

第一天:

先将要同步的文件给同步了一遍。

执行命令

rsync -auv /data/mysql/game /data1/mysql
rsync -auv /data/mysql/integral /data1/mysql
rsync -auv /data/mysql/interact /data1/mysql
rsync -auv /data/mysql/match /data1/mysql
rsync -auv /data/mysql/sns_admin /data1/mysql
rsync -auv /data/mysql/stock /data1/mysq

第二天:

又同步了一遍

rsync -auv /data/mysql/game /data1/mysql/game
rsync -auv /data/mysql/integral /data1/mysql/integral
rsync -auv /data/mysql/interact /data1/mysql/interact
rsync -auv /data/mysql/match /data1/mysql/match
rsync -auv /data/mysql/sns_admin /data1/mysql/sns_admin 
rsync -auv /data/mysql/stock /data1/mysql/stock

快要同步完的时候,检查文件大小,发现文件大了一倍。

原来第二步命令将/data/mysql/game文件 放在/data1/mysql/game文件下了。

这也不算是rsync的坑吧

应该这样这行

rsync -auv /data/mysql/game/ /data1/mysql/game/
rsync -auv /data/mysql/integral/ /data1/mysql/integral/
rsync -auv /data/mysql/interact/ /data1/mysql/interact/
rsync -auv /data/mysql/match/ /data1/mysql/match/
rsync -auv /data/mysql/sns_admin/ /data1/mysql/sns_admin/ 
rsync -auv /data/mysql/stock/ /data1/mysql/stock/

或者执行和昨天一样的命令

rsync -auv /data/mysql/game         /data1/mysql
rsync -auv /data/mysql/integral    /data1/mysql
rsync -auv /data/mysql/interact     /data1/mysql
rsync -auv /data/mysql/match        /data1/mysql
rsync -auv /data/mysql/sns_admin    /data1/mysql
rsync -auv /data/mysql/stock        /data1/mysq 

就可以了。

小结:

1、使用rsync同步,只要之前同步的内容和现在同步的内容有一点改变,就会重新全量同步,因此,同步的时候尽量找长时间没有变化的大文件
2、rsync不会追加文件

Linux下使用rsync同步文件

遇到的问题是几台游戏服务器,有一台新的服务器之前已经copy(Linux的scp命令)过文件上去,但在测试的过程中发现还是图片无法正常不显示出来,然后用httpwatch去分析请求的资源,发现有一个图片请求的地址是404(文件不存在),然后ssh登录上去,找到指定目录,发现确实是没有这个文件。。。

为了不至于再copy一次,就需要使用文件同步的管理软件了,同事推荐了linux下的rsync,小试了一下,还是很不错的。

假设有服务器A 和 服务器B,现在登录了服务器B,需要将服务器A上的文件同步到服务器B中,将A中有的B中没有的,或是A中文件比B中的文件要新就全部copy过来。

shell命令如下:

rsync -zvrtopg -progress -e 'ssh -p 端口号' [email protected]:/文件目录/  本机的目录地址

语法rsync -参数 ‘源文件访问路径’目标路径

第一次用的时候,我在源服务器(即上面所指的服务器A)中的文件目录没有加/导致后来直接把那个目录copy过来了,而不是它目录下的所有文件

关于rsync 这个命令的一些参数,英文好点就自己直接使用rsync –help去查看了,这里从其它人写的博客中转载过来的。

参考了:

rsync参数详解 http://hi.baidu.com/webv/blog/item/7c787a06581dab7e0208819a.html

rsync 使用说明 http://wandering.blog.51cto.com/467932/105113

使用之前需要先安装rsync,这个直接自行搜索吧..

关于参数的说明:

-v, --verbose 详细模式输出   
-q, --quiet 精简输出模式    
-c, --checksum 打开校验开关,强制对文件传输进行校验    
-a, --archive 归档模式,表示以递归方式传输文件,并保持所有文件属性,等于-rlptgoD    
-r, --recursive 对子目录以递归模式处理    
-R, --relative 使用相对路径信息    
rsync foo/bar/foo.c remote:/tmp/    

则在/tmp目录下创建foo.c文件,而如果使用-R参数:

rsync -R foo/bar/foo.c remote:/tmp/    

则会创建文件/tmp/foo/bar/foo.c,也就是会保持完全路径信息。

-b, --backup 创建备份,也就是对于目的已经存在有同样的文件名时,将老的文件重新命名为~filename。可以使用--suffix选项来指定不同的备份文件前缀。    
--backup-dir 将备份文件(如~filename)存放在在目录下。    
-suffix=SUFFIX 定义备份文件前缀    
-u, --update 仅仅进行更新,也就是跳过所有已经存在于DST,并且文件时间晚于要备份的文件。(不覆盖更新的文件)    
-l, --links 保留软链结    
-L, --copy-links 想对待常规文件一样处理软链结    
--copy-unsafe-links 仅仅拷贝指向SRC路径目录树以外的链结    
--safe-links 忽略指向SRC路径目录树以外的链结    
-H, --hard-links 保留硬链结    
-p, --perms 保持文件权限    
-o, --owner 保持文件属性信息    
-g, --group 保持文件属组信息    
-D, --devices 保持设备文件信息    
-t, --times 保持文件时间信息    
-S, --sparse 对稀疏文件进行特殊处理以节省DST的空间    
-n, --dry-run现实哪些文件将被传输    
-W, --whole-file 拷贝文件,不进行增量检测    
-x, --one-file-system 不要跨越文件系统边界    
-B, --block-size=SIZE 检验算法使用的块尺寸,默认是700字节    
-e, --rsh=COMMAND 指定替代rsh的shell程序    
--rsync-path=PATH 指定远程服务器上的rsync命令所在路径信息    
-C, --cvs-exclude 使用和CVS一样的方法自动忽略文件,用来排除那些不希望传输的文件    
--existing 仅仅更新那些已经存在于DST的文件,而不备份那些新创建的文件    
--delete 删除那些DST中SRC没有的文件    
--delete-excluded 同样删除接收端那些被该选项指定排除的文件    
--delete-after 传输结束以后再删除    
--ignore-errors 及时出现IO错误也进行删除    
--max-delete=NUM 最多删除NUM个文件    
--partial 保留那些因故没有完全传输的文件,以是加快随后的再次传输    
--force 强制删除目录,即使不为空    
--numeric-ids 不将数字的用户和组ID匹配为用户名和组名    
--timeout=TIME IP超时时间,单位为秒    
-I, --ignore-times 不跳过那些有同样的时间和长度的文件    
--size-only 当决定是否要备份文件时,仅仅察看文件大小而不考虑文件时间    
--modify-window=NUM 决定文件是否时间相同时使用的时间戳窗口,默认为0    
-T --temp-dir=DIR 在DIR中创建临时文件    
--compare-dest=DIR 同样比较DIR中的文件来决定是否需要备份    
-P 等同于 --partial    
--progress 显示备份过程    
-z, --compress 对备份的文件在传输时进行压缩处理    
--exclude=PATTERN 指定排除不需要传输的文件模式    
--include=PATTERN 指定不排除而需要传输的文件模式    
--exclude-from=FILE 排除FILE中指定模式的文件    
--include-from=FILE 不排除FILE指定模式匹配的文件    
--version 打印版本信息    
--address 绑定到特定的地址    
--config=FILE 指定其他的配置文件,不使用默认的rsyncd.conf文件    
--port=PORT 指定其他的rsync服务端口    
--blocking-io 对远程shell使用阻塞IO    
-stats 给出某些文件的传输状态    
--progress 在传输时现实传输过程    
--log-format=FORMAT 指定日志文件格式    
--password-file=FILE 从FILE中得到密码    
--bwlimit=KBPS 限制I/O带宽,KBytes per second    
-h, --help 显示帮助信息

一般常用的:

-v 详细模式输出

-r 对子目录以递归模式处理

-t 保持文件的时间信息

-o 保持文件的属性信息

-g 保持文件属组的信息(不清楚这样翻译是否有问题)

-p 保持文件权限不变

-e, --rsh=COMMAND 指定替代rsh的shell程序

-z, --compress 对备份的文件在传输时进行压缩处理

--progress 显示备份过程

也就是:递归、保证文件一些基本属性不变(创建/修改时间 权限等)、传输文件时压缩以减少带宽消耗,当然还有一个就看需要了:是否删除源中没有,而目标中又有的文件

PHP-FPM配置的优化

php-fpm默认安装后以下三个参数都是关闭的:

#表示在 emergency_restart_interval 所设值内出现SIGSEGV或者SIGBUS错误的php-cgi进程数如果

#超过 emergency_restart_threshold 个php-fpm就会优雅重启。这两个选项一般保持默认值。

emergency_restart_threshold = 10
emergency_restart_interval = 1m

#设置子进程接受主进程复用信号的超时时间. 可用单位: s(秒), m(分), h(小时), 或者 d(天) 默认单位: s(秒). 默认值: 0.

process_control_timeout = 0

出于优化的目的,我们把它们打开

emergency_restart_threshold = 10
emergency_restart_interval = 1m
process_control_timeout = 10s

有以下优点

在1分钟内,出现 SIGSEGV 或者 SIGBUS 错误的 PHP-CGI 进程数超到10个时,PHP-FPM 就会优雅的自动重启。

SIGBUS(Bus error)意味着指针所对应的地址是有效地址,但总线不能正常使用该指针。通常是未对齐的数据访问所致。
SIGSEGV(Segment fault)意味着指针所对应的地址是无效地址,没有物理内存对应该地址。

php-fpm 占用CPU过高,100%的解决方法

话说最近配置的LNMP还算稳定,正在暗自窃喜,但是从昨晚开始,就发现服务器的CPU占用过高,甚至到了100%。我的内存是1G的,正常情况下占用率应该在5%以下,最多不超10%。

阿里云最近的监控显示:

未分类

使用top命令查看,发现 php-fpm 占用内存过高,非常不正常:

未分类

我按照《Nginx使用的php-fpm的两种进程管理方式及优化》这篇文章,配置 php-fpm 进程数如下:

未分类

重启 php-fpm 后,还是没有彻底解决问题,依旧会出现占用 99以上,不知道哪位朋友知道如何分析和解决呢?小弟求助了!

后续进展

昨天(3月29日)找 @容哥 指导检查,后来发现是 eAccelerator 组件不知什么原因占用 CPU 过高,打开 php.ini

vi /usr/local/php/etc/php.ini

删除 eAccelerator 的配置信息,重启 lnmp

/root/lnmp restart

但还是没有彻底解决问题,几个小时后,我又重启了 lnmp ,居然发现,似乎已经越来越平稳啦。

可能用到的命令:

top                //查看CPU、内存使用信息,查看哪个进程占用CPU高以及它的PID

ll /proc/PID号/fd/             // 通过PID找到哪个文件操作的进程,进而知道问题所在

Nginx使用的php-fpm的两种进程管理方式及优化

这篇文章主要介绍了Nginx使用的php-fpm的两种进程管理方式及优化,需要的朋友可以参考下
PS:前段时间配置php-fpm的时候,无意中发现原来它还有两种进程管理方式。与Apache类似,它的进程数也是可以根据设置分为动态和静态的。

php-fpm目前主要又两个分支,分别对应于php-5.2.x的版本和php-5.3.x的版本。在5.2.x的版本中,php-fpm.conf使用的是xml格式,而在新的5.3.x版本中,则是和php.ini一样的配置风格。
在5.2.x版本中,php-fpm.conf中对于进程管理号称是有两种风格,一种是静态(static)的,一种是类似于apache风格(apache-like)的。

代码如下:

Process manager settings
<value name=”pm”>
Sets style of controling worker process count.
Valid values are 'static' and ‘apache-like'
<value name=”style”>static</value>

按照文档的说明,如果pm的style采用apache-like,启动的进程数应该是和StartServers指定的一样。不过经过数次的尝试,会发现,实际上在这里将pm的style配置成apache-like没有起任何作用。也就是说,这里的apache-like并没有被实现。
不过,在最新的5.3.x的配套php-fpm中,apache风格的进程管理已经被实现了。

代码如下:

; Choose how the process manager will control the number of child processes.
; Possible Values:
; static - a fixed number (pm.max_children) of child processes;
; dynamic - the number of child processes are set dynamically based on the
; following directives:
; pm.max_children - the maximum number of children that can
; be alive at the same time.
; pm.start_servers - the number of children created on startup.
; pm.min_spare_servers - the minimum number of children in 'idle'
; state (waiting to process). If the number
; of 'idle' processes is less than this
; number then some children will be created.
; pm.max_spare_servers - the maximum number of children in 'idle'
; state (waiting to process). If the number
; of 'idle' processes is greater than this
; number then some children will be killed.
; Note: This value is mandatory.
;pm = dynamic
pm = static

由上面一段文字可知,对于进程的管理存在两种风格——static和dynamic。和之前的版本的进程管理其实还是一样的,只是将apache-like改成了dynamic,这样更容易理解。

如果设置成static,php-fpm进程数自始至终都是pm.max_children指定的数量,不再增加或减少。
如果设置成dynamic,则php-fpm进程数是动态的,最开始是pm.start_servers指定的数量,如果请求较多,则会自动增加,保证空闲的进程数不小于pm.min_spare_servers,如果进程数较多,也会进行相应清理,保证多余的进程数不多于pm.max_spare_servers。

这两种不同的进程管理方式,可以根据服务器的实际需求来进行调整。

这里先说一下涉及到这个的几个参数,他们分别是pm、pm.max_children、pm.start_servers、pm.min_spare_servers和pm.max_spare_servers。
pm表示使用那种方式,有两个值可以选择,就是static(静态)或者dynamic(动态)。在更老一些的版本中,dynamic被称作apache-like。这个要注意看配置文件的说明。

下面4个参数的意思分别为:

  • pm.max_children:静态方式下开启的php-fpm进程数量。
  • pm.start_servers:动态方式下的起始php-fpm进程数量。
  • pm.min_spare_servers:动态方式下的最小php-fpm进程数量。
  • pm.max_spare_servers:动态方式下的最大php-fpm进程数量。

如果dm设置为static,那么其实只有pm.max_children这个参数生效。系统会开启设置数量的php-fpm进程。
如果dm设置为dynamic,那么pm.max_children参数失效,后面3个参数生效。系统会在php-fpm运行开始的时候启动pm.start_servers个php-fpm进程,然后根据系统的需求动态在pm.min_spare_servers和pm.max_spare_servers之间调整php-fpm进程数。

那么,对于我们的服务器,选择哪种执行方式比较好呢?事实上,跟Apache一样,运行的PHP程序在执行完成后,或多或少会有内存泄露的问题。这也是为什么开始的时候一个php-fpm进程只占用3M左右内存,运行一段时间后就会上升到20-30M的原因了。
对于内存大的服务器(比如8G以上)来说,指定静态的max_children实际上更为妥当,因为这样不需要进行额外的进程数目控制,会提高效率。因为频繁开关php-fpm进程也会有时滞,所以内存够大的情况下开静态效果会更好。数量也可以根据 内存/30M 得到,比如8GB内存可以设置为100,那么php-fpm耗费的内存就能控制在 2G-3G的样子。如果内存稍微小点,比如1G,那么指定静态的进程数量更加有利于服务器的稳定。这样可以保证php-fpm只获取够用的内存,将不多的内存分配给其他应用去使用,会使系统的运行更加畅通。
对于小内存的服务器来说,比如256M内存的VPS,即使按照一个20M的内存量来算,10个php-cgi进程就将耗掉200M内存,那系统的崩溃就应该很正常了。因此应该尽量地控制php-fpm进程的数量,大体明确其他应用占用的内存后,给它指定一个静态的小数量,会让系统更加平稳一些。或者使用动态方式,因为动态方式会结束掉多余的进程,可以回收释放一些内存,所以推荐在内存较少的服务器或VPS上使用。具体最大数量根据 内存/20M 得到。比如说512M的VPS,建议pm.max_spare_servers设置为20。至于pm.min_spare_servers,则建议根据服务器的负载情况来设置,比较合适的值在5~10之间。

php-fpm 搭建

php-fpm 即 php-Fastcgi Process Manager。
php-fpm 是 FastCGI 的实现,并提供了进程管理的功能。
进程包含 master 进程和 worker 进程两种进程。
master 进程只有一个,负责监听端口,接收来自 Web Server 的请求,而 worker 进程则一般有多个 (具体数量根据实际需要配置),每个进程内部都嵌入了一个 PHP 解释器,是 PHP 代码真正执行的地方。

配置 nginx 和 php-fpm 交互

  • 配置 nginx 文件
  location ~* .php$ {
    root           html;
    fastcgi_pass   127.0.0.1:9000;
    fastcgi_index  index.php;
    fastcgi_param  SCRIPT_FILENAME  $document_root$fastcgi_script_name;
    include        fastcgi_params;
  }

当请求的 url 能匹配以.php 结尾时,将反向代理给 php-fpm 处理动态信息。
root 是配置 php 程序放置的根目录。
fastcgi_pass 指令表示将该 url 请求代理至 nginx fastcgi 进程监听的 IP 地址和端口。
fastcgiparam 表示读取的是 $document_root(网站根目录)下的.php 文件;如果没有配置这一配置项,nginx 不会访问根目录的 php。

fastcgi_params 文件中含有各个 nginx 常量的定义,默认情况 SCRIPT_FILENAME=$fastcgi_script_name

openresty+redis拦截高频访问IP

CC攻击

网站受到攻击通常是黑客通过几个甚至多个IP地址,在短时间内进行超高频率访问,从而让服务器在短时间内增加巨大的计算量,导致负载增加,降低响应能力,甚至直接宕机停止服务。
通常这类情况我们只能通过查看分析网站日志,从而获得攻击者的IP地址,再通过防火墙进行拦截。
但一般而言这只会发生在监控系统已经开始报警之后,也就是网站或服务已经遭受到了攻击,并造成影响之后。并且在日志中搜寻到攻击者的IP并不是十分简单的事情,通常当我们找到了攻击者,攻击者可能已经停止了攻击,我们也只能预防他下次可能的攻击。

自动拦截

经历了黑客们深夜的骚扰和攻击,如何让那些短时间内大量访问的地址被自动拦截变成了努力的方向。
云服务商提供了WAF等商业化产品,协助我们处理这些威胁。
相比较于这些高昂价格的产品,开源软件同样在灵活性和可整合性上有很大的优势,接下来就介绍一下我是如何使用openresty和redis实现拦截高频访问的地址。

安装环境

之前的文章已经介绍过:Openresty+Redis 动态切换upstream (http://learn-learn.top/archives/169.html)
大致按照官方介绍就可以轻松安装。

nginx配置

nginx在初始化时建立一个redis的链接,并且在每次访问前需要执行block.lua进行验证

init_by_lua_block {
    redis = require "redis"
    client = redis.connect('127.0.0.1', 6379)
}
server {
    listen 8080;
    location  / {
        access_by_lua_file /usr/local/nginx/conf/lua/block.lua; 
        proxy_pass http://192.168.1.102:8000;
    }
}

lua脚本:

function isConnected()
    return client:ping()
end
function createRedisConnection()
        return redis.connect('127.0.0.1', 6379)
end

if pcall(isConnected)then --如果发生redis连接失败,将停止拦截。
    --
else
    if pcall(createRedisConnection)then     --断开重连会发送每次访问都需要重连redis
        client = createRedisConnection();       --如果访问量大的情况下,建议关闭重连,if pcall不执行,直接ngx.exit
    else
        ngx.exit(ngx.OK);
    end 
end


local ttl = 60;     --监测周期
local bktimes = 30; --在监测周期内达到触发拦截的访问量
block_ttl = 600;    --触发拦截后拦截时间
ip = ngx.var.remote_addr
ipvtimes = client:get(ip)

if(ipvtimes)then
    if(ipvtimes == "-1")then
        --ngx.say("blocked")
        return ngx.exit(403);
    else
        last_ttl = client:ttl(ip)
        --ngx.say("key exist.ttl is ",last_ttl);
        if(last_ttl==-1)then
            client:set(ip,0)
            client:expire(ip,ttl)
            --ngx.say("ttl & vtimes recount")
            return ngx.exit(ngx.OK);
        end
        vtimes = tonumber(client:get(ip))+1;
        if(vtimes<bktimes)then
            client:set(ip,vtimes);
            client:expire(ip,last_ttl)
            --ngx.say(ip," view ",vtimes," times");
            return ngx.exit(ngx.OK);
        else
            --ngx.say(ip," will be block noext time.")
            client:set(ip,-1);
            client:expire(ip,block_ttl)
            return ngx.exit(ngx.OK);
        end
    end
else
    --ngx.say("key do not exist")
    client:set(ip,1)
    --ngx.say(ip," view 1 times")
    client:expire(ip,ttl)
    return ngx.exit(ngx.OK)
end

脚本说明:

1.重要参数:

ttl = 60; –监测周期
bktimes = 30; –在监测周期内达到触发拦截的访问量
block_ttl = 600; –触发拦截后拦截时间

以上参数表示,一个IP地址在60秒内访问超过30次将被拦截600秒。

2.逻辑说明:

a)检测初始化的redis连接是否能够正常运行,如果连接失败或已经断开,将会重新建立连接,如果仍旧无法连接,将直接放行。这里是为了避免redis宕机导致nginx无法正常响应。当然如果初始连接中断,将会导致每次访问都会创建redis连接。

b)当某个IP首次访问时,将在redis中新建一个以IP地址为KEY的键(如果需要多个站点,修改下key的命名规则即可),value为1,并设置expire时间。当这个地址再次访问且key尚未过期前,将会每次递增key的value数,直到到达达到bktimes,或者key到期从而消亡。(本人用的redis5.0,到期key会直接不存在,可能部分版本到期后value为-1)

c)当key过期后,对于系统而言就是第一次访问,重新创建value为1的新key

d)当达到bktimes后,会将对应的IP的key的value设置为-1,且过期时间为block_ttl。

e)当访问到value为-1的key,即某个IP达到了我们设定的访问频次,我们将直接拦截,返回403.

3.完善方向:

a)在访问前添加黑白名单功能(这个在redis中新建立两个key即可)
b)拦截IP段(根据访问的IP地址建立以IP段为key的字段即可)
c)redis断开重连后,每次访问都要建立连接问题。

Openresty的同步输出与流式响应

默认情况下, ngx.say和ngx.print都是异步输出的,先来看一个例子:

location /test {
    content_by_lua_block {
        ngx.say("hello")
        ngx.sleep(3)
        ngx.say("the world")
    }
}

执行测试,可以发现首先, /test 响应内容是在触发请求 3s 后一起接收到响应体,第一个ngx.say好像是被“绕过”,先执行sleep,然后和最后一个ngx.say的内容一起输出。

location /test {
    content_by_lua_block {
        ngx.say("hello")
        ngx.flush() -- 显式的向客户端刷新响应输出
        ngx.sleep(3)
        ngx.say("the world")
    }
}

首先输出”hello”,然后停顿3秒,最后输出”the world”——正如我们想象的那样。ngx.flush执行显示的输出,前一个ngx.say被“阻塞”住,执行完输出后方往下执行。

再看一个例子:

server {
    listen 80;
    lua_code_cache off;
    location /test {
        content_by_lua_block {
            ngx.say(string.rep("hello", 4000))
            ngx.sleep(3)
            ngx.say("the world")
        }
    }
}

这个例子和第一个例子相比,唯一不同就是ngx.say输出内容长了不少,我们发现浏览器先收到所有的hello,接着又收到了”the world” 。然而如果我们把4000改为小一点的值如2000(不同配置这个相对大小或有不同),那么仍然会出现先停顿3s,然后所有”hello”连同最后”the world”一起输出的情况。

通过以上三个例子,我们可以得出下面的结论:

ngx.say和ngx.print的同步和异步

  • nginx有个输出缓冲(system send buffer),如16k。ngx.say和ngx.print默认是向这个输出缓冲写入数据,如果没有显示的调用ngx.flush,那么在content阶段结束后输出缓冲会写入客户端;

  • 如果没有ngx.flush也没有到结束阶段,但如果输出缓冲区满了,那么也会输出到客户端;

因此ngx.say和ngx.print的默认向客户端的输出都是异步的,非实时性的,改变这一行为的是ngx.flush,可以做到同步和实时输出。这在流式输出,比如下载大文件时非常有用。

ngx.flush的同步和异步

lua-nginx也提到了ngx.flush的同步和异步。某一个ngx.say或者ngx.print调用后,这部分输出内容会写到输出缓冲区,同步的方式ngx.flush(true)会等到内容全部写到缓冲区再输出到客户端,而异步的方式ngx.flush()会将内容一边写到缓冲区,而缓冲区则一边将这些内容输出到客户端。

openresty和nginx流式输出的比较

流式输出,或者大文件的下载,nginx的upstream模块已经做得非常好,可以通过proxy_buffering|proxy_buffer_size|proxy_buffers 等指令精细调控,而且这些指令的默认值已经做了妥善处理。我们来看看这些指令以及默认值:

proxy_buffering on;
proxy_buffer_size 4k|8k; 
proxy_buffers 8 4k|8k; 
proxy_busy_buffers_size 8k|16k;
proxy_temp_path proxy_temp;
  • proxy_buffering on表示内存做整体缓冲,内存不够时多余的存在由proxy_temp_path指定的临时文件中,off表示每次从上游接收proxy_buffer_size响应的内容然后直接输出给客户端,不会试图缓冲整个响应
  • proxy_buffer_size和proxy_buffers都是指定内存缓冲区的大小,proxy_buffer_size通常缓冲响应头,proxy_buffers缓冲响应内容,默认为一页的大小,proxy_buffers还可以指定这样的缓冲区的个数
  • proxy_busy_buffers_size nginx在试图缓冲整个响应过程中,可以让缓冲区proxy_busy_buffers_size大小的已经写满的部分先行发送给客户端。于此同时,缓冲区的另外部分可以继续读。如果内存缓冲区不够用了,还可以写在文件缓冲区
  • proxy_temp_path 使用文件作为接受上游请求的缓冲区buffer,当内存缓冲区不够用时启用

openresty的怎么做到过大响应的输出呢? (https://moonbingbing.gitbooks.io/openresty-best-practices/content/index.html) 提到了两种情况:

  • 输出内容本身体积很大,例如超过 2G 的文件下载
  • 输出内容本身是由各种碎片拼凑的,碎片数量庞大

前面一种情况非常常见,后面一种情况比如上游已经开启Chunked的传输方式,而且每片chunk非常小。笔者就遇到了一个上游服务器通过Chunked分片传输日志,而为了节省上游服务器的内存将每片设置为一行日志,一般也就几百字节,这就太“碎片”了,一般日志总在几十到几百M,这么算下来chunk数量多大10w+。笔者用了resty.http来实现文件的下载,文件总大小48M左右。

local http = require "resty.http"
local httpc = http.new()

httpc:set_timeout(6000)
httpc:connect(host, port)

local client_body_reader, err = httpc:get_client_body_reader()

local res, err = httpc:request({
    version = 1.1,
    method = ngx.var.request_method,
    path = ngx.var.app_uri,
    headers = headers,
    query = ngx.var.args,
    body = client_body_reader
})

if not res then
    ngx.say("Failed to request ".. ngx.var.app_name .." server: ", err)
    return
end

-- Response status
ngx.status = res.status

-- Response headers
for k, v in pairs(res.headers) do
    if k ~= "Transfer-Encoding" then  --必须删除上游Transfer-Encoding响应头
        ngx.header[k] = v
    end
end

-- Response body
local reader = res.body_reader
repeat
    local chunk, err = reader(8192)
    if err then
        ngx.log(ngx.ERR, err)
        break
    end

    if chunk then
        ngx.print(chunk)
        ngx.flush(true)  -- 开启ngx.flush,实时输出
    end
until not chunk

local ok, err = httpc:set_keepalive()
if not ok then
    ngx.say("Failed to set keepalive: ", err)
    return
end

多达10w+的”碎片”的频繁的调用ngx.pirnt()和ngx.flush(true),使得CPU不堪重负,出现了以下的问题:

  • CPU轻轻松松冲到到100%,并保持在80%以上
  • 由于CPU的高负荷,实际的下载速率受到显著的影响
  • 并发下载及其缓慢。笔者开启到第三个下载连接时基本就没有反应了

未分类

这是开启了ngx.flush(true)的情况(ngx.flush()时差别不大),如果不开启flush同步模式,则情况会更糟糕。CPU几乎一直维持在100%左右:

未分类

可见,在碎片极多的流式传输上,以上官方所推荐的openresty使用方法效果也不佳。

于是,回到nginx的upstream模块,改content_by_lua_file为proxy_pass再做测试,典型的资源使用情况为:

未分类

无论是CPU还是内存占用都非常低,开启多个下载链接后并无显著提升,偶尔串升到30%但迅速下降到不超过10%。

因此结论是,涉及到大输出或者碎片化响应的情况,最好还是采用nginx自带的upstream方式,简单方便,精确控制。而openresty提供的几种方式,无论是异步的ngx.say/ngx.print还是同步的ngx.flush,实现效果都不理想。

NFS问题:clnt_create: RPC: Port mapper failure – Unable to receive: errno 113 (No route to host)

摘要:NFS问题:nfs与rpcbind都正常启动了,并且已经发布共享目录/tmp。

在客户端查看时,出现如下错误提示:

[[email protected]~]# showmounte192.168.122.10
clnt_create:RPC:Portmapperfailure-Unabletoreceive:errno113(Noroutetohost)

解决方法:关闭被访问的NFS服务器上的防火墙和selinux(当然只适合个人测试环境),执行systemctlstopfire

NFS问题:

nfs与rpcbind都正常启动了,并且已经发布共享目录/tmp。

在客户端查看时,出现如下错误提示:

[[email protected] ~]# showmount -e 192.168.122.10
clnt_create: RPC: Port mapper failure - Unable to receive: errno 113 (No route to host)

解决方法:

关闭被访问的NFS服务器上的防火墙和selinux(当然只适合个人测试环境)

执行

systemctl stop firewalld
iptables -F
setenforce 0

在客户端重新查看showmount -e 192.168.122.10

在客户端showmount -e 192.168.122.10

遇到以下错误提示“clnt_create: RPC: Program not registered”

解决方法:

服务端[[email protected] /]# rpc.mountd 即可

客户端查看

[[email protected] ~]# showmount -e 192.168.122.10

Export list for server.example.com:/tmp *(rw,sync,no_root_squash)