写了一个头条爬虫,放到服务器上使用crontab定时采集头条文章,服务器系统是Ubuntu 16.04 LTS,一开始设置的是间隔4小时采集一次,运行起来很正常,不过一到下午4点采集任务跑起来就超级超级慢,后来就改成离散的时间点采集,跑了两天发现,crontab定时任务执行的时间与系统时间不一致,总是差了8个小时,而不是在设置的时间点执行,查看系统时区,是北京时间呀,怎么会不一样,不过时间差总是8个小时,估计应该是crontab的时区跟系统的时区不一样,重启一下cron服务

sudo service cron restart

又跑了两天,发现执行时间正常了,按照设置的时间点执行了,看来问题就是系统时区的配置晚于cron服务启动的时间,也就是系统时区改动后未重启cron服务!

最近使用Python WordPress XML-RPC模块把采集的文章发布到WordPress,不过发现偶尔会报 Fault -32700: 'parse error. not well formed' 的错误,大部分文章都能正常发布,只有极个别文章发布的时候会报这个错误。

因为WordPress XML-RPC模块是使用xml发布文章的,当文章内容中带有xml中不支持的字符时,也就是xml文件中含有非法字符时,是一个Bad XML,不符合xml规范的。

检查一下报错的文章,发现有字符\x08,而xml允许的字符范围#x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] | [#x10000-#x10FFFF],因此在使用xml时需要把这个范围之外的字符过滤掉,也就是说

\x00-\x08
\x0b-\x0c
\x0e-\x1f

这三组字符是不允许出现在XML中的,解决方法很简单,用正则表达式替换

re.sub("[\x00-\x08\x0b-\x0c\x0e-\x1f]", "", text)

在VPS上用pip安装一个python包时,直接提示Killed退出了

#Output
Installing collected packages: pypinyin
Killed

无法安装,好奇怪,别的包都安装上了,就这个出问题,在stackoverflow搜到一个问答,https://stackoverflow.com/questions/43245196/pip-install-killed ,貌似是内存不足导致的。

按照问答里说的,查看Linux kernel日志

tail -f /var/log/kern.log
#Output
Out of memory: Kill process 18741 (pip) score 251 or sacrifice child
Killed process 18741 (pip) total-vm:178620kB, anon-rss:121956kB, file-rss:2996kB

看来确实是内存不足,Out of memory,然后直接被系统kill掉了。

内存不够,增加swap交换分区,正好vps用的是Ubuntu系统,直接参照这篇文章 https://www.digitalocean.com/community/tutorials/how-to-add-swap-space-on-ubuntu-16-04

Ubuntu添加Swap分区

查看系统是否有Swap分区

sudo swapon --show

如果没有任何输出,表示没有swap分区,也可以通过free命令验证一下

free -h
#Output
              total        used        free      shared  buff/cache   available
Mem:            486         316          81           5          88         132
Swap:             0           0           0

可以看到Swap的大小是零,也就是没有swap分区。

创建Swap分区文件

创建一个1G大小的文件作为交换分区的文件

sudo fallocate -l 1G /swapfile

设置这个文件权限

sudo chmod 600 /swapfile

格式化为交换分区文件

sudo mkswap /swapfile

启用交换分区文件

sudo swapon /swapfile

验证一下swap分区文件是否创建成功

sudo swapon --show
#Output
NAME      TYPE  SIZE USED PRIO
/swapfile file 1024M   0B   -1

设置开机时自启用Swap分区

备份/etc/fstab文件,防止出错

sudo cp /etc/fstab /etc/fstab.bak

输入下面的命令在/etc/fstab文件添加一行swap文件的信息

echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab

优化Swap设置

调整Swappiness值

sudo sysctl vm.swappiness=10
#Output
vm.swappiness = 10

调整Cache Pressure值

sudo sysctl vm.vfs_cache_pressure=50
#Output
vm.vfs_cache_pressure = 50

设置开机时自动调整这两项设置值
/etc/sysctl.conf文件最后添加下面两行内容

vm.swappiness=10
vm.vfs_cache_pressure=50

OK,搞定,测试一下看pip是否能安装这个包了,哈哈,不再是Killed,变成了Successfully installed,没有问题,成功安装。

#Output
Installing collected packages: pypinyin
Successfully installed pypinyin-0.33.0

2021.3.15更新

网站套上了Cloudflare CDN后,因为服务器隐藏在cloudflare的后端,所以网站实际接收到访问请求都是Cloudflare的节点服务器发送过来的,那么访客IP也都是Cloudflare的节点服务器的IP,不是访客真实的IP。

如果只是需要nginx的日志记录访客的真实IP,那么不需要开启ngx_http_realip_module模块,只需要调整一下nginx的日志格式log_format即可。

Cloudflare 会将原始访问者 IP 地址包含在 X-Forwarded-For 和 CF-Connecting-IP 请求标头header中,我们只需要增加一个新的nginx log_format。

Nginx的log_format有一个默认的combined格式,具体可以参考nginx的帮助文档http://nginx.org/en/docs/http/ngx_http_log_module.html#log_format

log_format combined '$remote_addr - $remote_user [$time_local] '
                    '"$request" $status $body_bytes_sent '
                    '"$http_referer" "$http_user_agent"';

增加一个新的log_format:

1、编辑nginx.conf文件 sudo vim /usr/local/nginx/conf/nginx.conf

2、在sever指令加载前添加新的log_format,并保存

log_format csf '$http_x_forwarded_for - $remote_user [$time_local] '
               '"$request" $status $body_bytes_sent '
               '"$http_referer" "$http_user_agent"';

3、对需要开启access log的网站,编辑对应的配置文件 sudo vim /usr/local/nginx/conf/vhost/example.site.conf

4、在对应的sever指令中加入相应的access_log设置

access_log /home/wwwlogs/example.site.access.log csf;

5、重启Nginx,sudo lnmp nginx restart

这样,Nginx的访问日志记录的就是访客的真实IP了,当然如果不只是需要在access log中记录访客真实IP,那可以继续往下看哦。


2018.8.20分割线

写了一个小网站,刚开始估计也没什么流量,打算开启一下nginx的访问日志access_log,不过问题来了,因为上线时直接套上了Cloudflare CDN,结果发现访问日志里的IP全是Cloudflare节点服务器IP,不是访客的真实IP,这有点尴尬了,拿不到真实IP访问日志就没啥用了,不好分析哪个访问是蜘蛛,哪个访问是真实访客,想着这个问题应该有解决方案,直接在Cloudflare帮助里搜了一下,可以可以,确实有,https://support.cloudflare.com/hc/en-us/articles/200170706-How-do-I-restore-original-visitor-IP-with-Nginx,可以用nginx的 ngx_http_realip_module 模块来获取访客的真实IP,那还说什么,赶紧操作起来。

在网站的nginx配置文件里,加上Cloudflare提供的配置参数

set_real_ip_from 103.21.244.0/22;
set_real_ip_from 103.22.200.0/22;
set_real_ip_from 103.31.4.0/22;
set_real_ip_from 104.16.0.0/12;
set_real_ip_from 108.162.192.0/18;
set_real_ip_from 131.0.72.0/22;
set_real_ip_from 141.101.64.0/18;
set_real_ip_from 162.158.0.0/15;
set_real_ip_from 172.64.0.0/13;
set_real_ip_from 173.245.48.0/20;
set_real_ip_from 188.114.96.0/20;
set_real_ip_from 190.93.240.0/20;
set_real_ip_from 197.234.240.0/22;
set_real_ip_from 198.41.128.0/17;
set_real_ip_from 2400:cb00::/32;
set_real_ip_from 2606:4700::/32;
set_real_ip_from 2803:f800::/32;
set_real_ip_from 2405:b500::/32;
set_real_ip_from 2405:8100::/32;
set_real_ip_from 2c0f:f248::/32;
set_real_ip_from 2a06:98c0::/29;

# use any of the following two
real_ip_header CF-Connecting-IP;
#real_ip_header X-Forwarded-For;

大概意思就是列表里的ip访问时,会在请求的header参数CF-Connecting-IPX-Forwarded-For中添加访客的真实IP,ngx_http_realip_module模块会在列表中的IP访问时,直接从CF-Connecting-IP中获取访客的真实IP,这些IP是Cloudflare的节点服务器IP,可能会不定时的更新,在这里 https://www.cloudflare.com/ips/ 可以获取到最新的Cloudflare节点服务器IP列表。

配置好后,重载nginx,/etc/init.d/nginx reload,报错

Reload service nginx... nginx: [emerg] unknown directive "set_real_ip_from" in /usr/local/nginx/conf/vhost/xxx.com.conf:10
 done

这是因为ngx_http_realip_module模块默认是不启用的,需要在编译安装时添加配置参数 --with-http_realip_module 开启,然后重新编译安装一下nginx。

如果用的是lnmp一键安装脚本,那就改一下脚本里的nginx编译配置参数,lnmp1.5版本的在 include/nginx.sh 文件的70行

   if echo ${Nginx_Ver} | grep -Eqi 'nginx-[0-1].[5-8].[0-9]' || echo ${Nginx_Ver} | grep -Eqi 'nginx-1.9.[1-4]$'; then
        ./configure --user=www --group=www --prefix=/usr/local/nginx --with-http_stub_status_module --with-http_ssl_module --with-http_spdy_module --with-http_gzip_static_module --with-ipv6 --with-http_sub_module ${Nginx_With_Openssl} ${Nginx_Module_Lua} ${NginxMAOpt} ${Nginx_Modules_Options}
    else
        ./configure --user=www --group=www --prefix=/usr/local/nginx --with-http_stub_status_module --with-http_ssl_module --with-http_v2_module --with-http_gzip_static_module --with-http_sub_module --with-stream --with-stream_ssl_module ${Nginx_With_Openssl} ${Nginx_Module_Lua} ${NginxMAOpt} ${Nginx_Modules_Options}
    fi  

在编译配置参数里加上 --with-http_realip_module

   if echo ${Nginx_Ver} | grep -Eqi 'nginx-[0-1].[5-8].[0-9]' || echo ${Nginx_Ver} | grep -Eqi 'nginx-1.9.[1-4]$'; then
        ./configure --user=www --group=www --prefix=/usr/local/nginx --with-http_realip_module --with-http_stub_status_module --with-http_ssl_module --with-http_spdy_module --with-http_gzip_static_module --with-ipv6 --with-http_sub_module ${Nginx_With_Openssl} ${Nginx_Module_Lua} ${NginxMAOpt} ${Nginx_Modules_Options}
    else
        ./configure --user=www --group=www --prefix=/usr/local/nginx --with-http_realip_module --with-http_stub_status_module --with-http_ssl_module --with-http_v2_module --with-http_gzip_static_module --with-http_sub_module --with-stream --with-stream_ssl_module ${Nginx_With_Openssl} ${Nginx_Module_Lua} ${NginxMAOpt} ${Nginx_Modules_Options}
    fi  

lnmp1.5支持单独安装nginx,所以只需要重新编译安装一下nginx即可,安装包目录下运行

./install.sh nginx

等上几分钟,编译安装nginx完成,OK,nginx的访问日志记录的是访客的真实IP了,当然不只是Cloudflare CDN,如果用其他的CDN,这种方法也可以的,不过配置里相应的节点IP要换成对应的CDN服务商提供的IP。

PS,如果跑affiliate markting的话,可能会用到追踪系统,比如iMobiTrax这样的可以自建的追踪系统,然后会发现在加上cloudfalre后,访客日志报表里记录的IP全是cloudflare的节点服务器IP,不是真实的访客IP,哈哈,是不是很尴尬,而且iMobiTrax又是加密的,没有办法通过修改源代码来获取真实IP,之前用的时候遇到过,找了很久,没有找到一个可用的解决方案,最后不了了之只好把cloudflare去掉了,不过,今天遇到这个问题正好想到了,也测试了一下,用上边的修改nginx的方式是可以的哦,给nginx添加ngx_http_realip_module模块,然后修改一下相应配置,就可以愉快的给iMobiTrax这样的追踪系统套上Cloudflare CDN了,再也不怕获取不到访客的真实IP了,当然不仅是iMobiTrax,其他的程序也一样,因为这样nginx会直接把真实IP传递给程序,不需要在程序里修改代码来获取真实IP了。

之前写的一个python小程序,在windows服务器上跑的还可以,偶尔出点小意外,打算换到linux服务器上去跑,本来想着应该没啥问题,结果把环境配好以后,一运行,出问题了,报错 FileNotFoundError: [Errno 2] No such file or directory ,这是什么鬼,找不到文件???而且还是在windows环境下能跑,linux环境下跑就出问题了?看错误信息,好像是subprocess模块报的错,难道是环境没配好?子程序缺文件?不能运行?直接在shell里运行一下子命令看看,没有问题啊,可以正常执行的,这还真是奇怪了。

google了一下,发现是一个参数惹的祸,shell=False,因为这个程序用了subprocess模块来执行一些子程序

subprocess.run(args, *, stdin=None, input=None, stdout=None, stderr=None, shell=False, cwd=None, timeout=None, check=False, encoding=None, errors=None, text=None, env=None)

shell参数默认是False,因为在windows和linux环境下subprocess模块执行子程序的方式不一样,官方文档是这样写的

Execute a child program in a new process. On POSIX, the class uses os.execvp()-like behavior to execute the child program. On Windows, the class uses the Windows CreateProcess() function. The arguments to Popen are as follows.
args should be a sequence of program arguments or else a single string. By default, the program to execute is the first item in args if args is a sequence. If args is a string, the interpretation is platform-dependent and described below. See the shell and executable arguments for additional differences from the default behavior. Unless otherwise stated, it is recommended to pass args as a sequence.
On POSIX, if args is a string, the string is interpreted as the name or path of the program to execute. However, this can only be done if not passing arguments to the program.
On Windows, if args is a sequence, it will be converted to a string in a manner described in Converting an argument sequence to a string on Windows. This is because the underlying CreateProcess() operates on strings.
The shell argument (which defaults to False) specifies whether to use the shell as the program to execute. If shell is True, it is recommended to pass args as a string rather than as a sequence.
On POSIX with shell=True, the shell defaults to /bin/sh. If args is a string, the string specifies the command to execute through the shell. This means that the string must be formatted exactly as it would be when typed at the shell prompt. This includes, for example, quoting or backslash escaping filenames with spaces in them. If args is a sequence, the first item specifies the command string, and any additional items will be treated as additional arguments to the shell itself.
On Windows with shell=True, the COMSPEC environment variable specifies the default shell. The only time you need to specify shell=True on Windows is when the command you wish to execute is built into the shell (e.g. dir or copy). You do not need shell=True to run a batch file or console-based executable.
args is required for all calls and should be a string, or a sequence of program arguments. Providing a sequence of arguments is generally preferred, as it allows the module to take care of any required escaping and quoting of arguments (e.g. to permit spaces in file names). If passing a single string, either shell must be True or else the string must simply name the program to be executed without specifying any arguments.

所以在linux环境下,当shell=False(默认)时,subprocess.Popen使用os.execvp()来执行子程序,args参数需要是一个列表,如果args参数是个字符串的话,会被当做是命令本身或者可执行文件的路径,也就是说字符串只能是命令本身,而不能有额外的命令参数,如果字符串里带有命令参数的话,那参数也会被当作命令本身传递给os.execvp()来执行,比如说你要执行 cat test.txt, cat test.txt这整个字符串都会被当作可执行文件的路径来执行,但是又不存在cat test.txt这样的一个可执行的命令或者可执行文件的路径,所以就会报FileNotFoundError: [Errno 2] No such file or directory这样的错了。

shell=True时,会直接使用shell来执行子程序,如果args参数是字符串会直接传递给shell执行,如果args参数是个列表,args[0] 被视为命令交给shell执行,args[1:] 则会被忽略或者如果是可以改变shell行为的有效参数则对shell做相应调整,比如subprocess.run('cat test.txt', shell=True)正常执行查看test.txt文件,而subprocess.run(['cat', 'test.txt'], shell=True)则只会执行cat命令,而不会查看test.txt文件,而test.txt会被当作shell本身的参数,但是test.txt又不是shell的有效参数所以被忽略。

简单说就是当args参数是个字符串时,需要设置shell=True,当args参数是个列表的时候,shell保持默认的False。