后端nginx使用set_real_ip_from获取用户真实IP

随着nginx的迅速崛起,越来越多公司将apache更换成nginx. 同时也越来越多人使用nginx作为负载均衡, 并且代理前面可能还加上了CDN加速,但是随之也遇到一个问题:nginx如何获取用户的真实IP地址.
实例环境:

  • 用户IP 120.22.11.11
  • CDN前端 61.22.22.22
  • CDN中转 121.207.33.33
  • 公司NGINX前端代理 192.168.50.121(外网121.207.231.22)

1、使用CDN自定义IP头来获取

假如说你的CDN厂商使用nginx,那么在nginx上将$remote_addr赋值给你指定的头,方法如下:

proxy_set_header remote-user-ip $remote_addr;

后端PHP代码getRemoteUserIP.php

<?php
    $ip = getenv("HTTP_REMOTE_USER_IP");
    echo $ip;    
?>

访问getRemoteUserIP.php,结果如下:

120.22.11.11 //取到了真实的用户IP,如果CDN能给定义这个头的话,那这个方法最佳

2、通过HTTP_X_FORWARDED_FOR获取IP地址

一般情况下CDN服务器都会传送HTTP_X_FORWARDED_FOR头,这是一个ip串,后端的真实服务器获取HTTP_X_FORWARDED_FOR头,截取字符串第一个不为unkown的IP作为用户真实IP地址, 例如:
120.22.11.11,61.22.22.22,121.207.33.33,192.168.50.121(用户IP,CDN前端IP,CDN中转,公司NGINX代理)
getFor.php

<?php
    $ip = getenv("HTTP_X_FORWARDED_FOR");
    echo $ip;
?>

访问getFor.php结果如下:

120.22.11.11,61.22.22.22,121.207.33.33,192.168.50.121

如果你是php程序员,你获取第一个不为unknow的ip地址,这边就是120.22.11.11.

3.使用nginx自带模块realip获取用户IP地址

安装nginx之时加上realip模块,我的参数如下:

    ./configure --prefix=/usr/local/nginx-1.4.1 --with-http_realip_module

真实服务器nginx配置

    server {
        listen       80;
        server_name  www.ttlsa.com;
        access_log  /data/logs/nginx/www.ttlsa.com.access.log  main;

        index index.php index.html index.html;
        root /data/site/www.ttlsa.com;

        location /
        {
                root /data/site/www.ttlsa.com;
        }
        location = /getRealip.php
        {
                set_real_ip_from  192.168.50.0/24;
                set_real_ip_from  61.22.22.22;
                set_real_ip_from  121.207.33.33;
                set_real_ip_from 127.0.0.1;
                real_ip_header    X-Forwarded-For;
                real_ip_recursive on;
                fastcgi_pass  unix:/var/run/phpfpm.sock;
                fastcgi_index index.php;
                include fastcgi.conf;
        }
    }

getRealip.php内容

    <?php
        $ip =  $_SERVER['REMOTE_ADDR'];
        echo $ip;    
    ?>

访问www.ttlsa.com/getRealip.php,返回:

    120.22.11.11

如果注释 real_ip_recursive on或者 real_ip_recursive off
访问www.ttlsa.com/getRealip.php,返回:

121.207.33.33

很不幸,获取到了中继的IP,real_ip_recursive的效果看明白了吧.

  • set_real_ip_from:真实服务器上一级代理的IP地址或者IP段,可以写多行
  • real_ip_header:从哪个header头检索出要的IP地址
  • real_ip_recursive:递归排除IP地址,ip串从右到左开始排除set_real_ip_from里面出现的IP,如果出现了未出现这些ip段的IP,那么这个IP将被认为是用户的IP。例如我这边的例子,真实服务器获取到的IP地址串如下:
120.22.11.11,61.22.22.22,121.207.33.33,192.168.50.121

在real_ip_recursive on的情况下
61.22.22.22,121.207.33.33,192.168.50.121都出现在set_real_ip_from中,仅仅120.22.11.11没出现,那么他就被认为是用户的ip地址,并且赋值到remote_addr变量
在real_ip_recursive off或者不设置的情况下
192.168.50.121出现在set_real_ip_from中,排除掉,接下来的ip地址便认为是用户的ip地址
如果仅仅如下配置:

    set_real_ip_from   192.168.50.0/24;
    set_real_ip_from 127.0.0.1;
    real_ip_header    X-Forwarded-For;
    real_ip_recursive on;

访问结果如下:

   121.207.33.33

4、三种在CDN环境下获取用户IP方法总结

4.1 CDN自定义header头

  • 优点:获取到最真实的用户IP地址,用户绝对不可能伪装IP
  • 缺点:需要CDN厂商提供

4.2 获取forwarded-for头

  • 优点:可以获取到用户的IP地址
  • 缺点:程序需要改动,以及用户IP有可能是伪装的

4.3 使用realip获取

  • 优点:程序不需要改动,直接使用remote_addr即可获取IP地址
  • 缺点:ip地址有可能被伪装,而且需要知道所有CDN节点的ip地址或者ip段

使用nginx ngx_http_memcached_module及memcached实现页面缓存

页面静态化是前端优化的一个重要方法,一般采用生成静态文件的方式实现。这里我尝试采用另外一种方式去实现,就是直接把页面用Memcached进行缓存,然后通过Nginx直接去访问。

采用Memcached缓存页面的好处是什么呢?

  1. 由于页面是缓存在内存里,所以减少了系统的I/O操作。
  2. 可以直接利用Memcached的分布式特性。
  3. 可以直接利用缓存的过期时间,方便对页面的过期时间进行处理。
  4. 部署简单,生成静态文件还需要考虑文件系统的问题。
    当然缺点也很明显,首先是对内存的性能依赖很大,其次由于页面直接放内存里,一旦Memcached挂掉或者服务器重启,内存里存储的页面就会全部消失。

问题来了

Nginx内置了Memcached模块ngx_http_memcached_module,可以很轻松的实现对Memcached的访问。我这里做一个示例,通过PHP缓存我们邮轮网站的首页,然后通过URLhttp://dev.hwtrip.com/cache/index.html去访问这个页面。

首先,我们对Nginx进行配置:

server {
    listen       80;
    server_name  dev.hwtrip.com;

    location ^~ /cache/ {
        set            $memcached_key $request_uri;
        memcached_pass 127.0.0.1:11211;
    }

    error_page     404 502 504 = @fallback;
}

location @fallback {
    proxy_pass     http://backend;
}

这个配置把所有请求URI前缀为/cache/的访问用Memcached模块进行内容的读取,同时使用请求URI作为Memcached的key。当缓存没有命中或者出错时,我们使用@fallback进行处理(例如访问实际的应用并重新写入缓存),这个不在这里展开了。

然后,我们用简单的代码把页面写进Memcached里:


$htmlContent = file_get_contents('http://youlun.hwtrip.com'); $memcached = new Memcache(); $memcached->addServer('127.0.0.1', 11211); $memcached->set('/cache/index.html', $htmlContent);

注意写缓存时的key,由于我们访问的URL是http://dev.hwtrip.com/cache/index.html,所以写进Memcached的key就是URI/cache/index.html。

执行完代码后,我们访问下http://dev.hwtrip.com/cache/index.html:
Nginx

可以看到,通过nginx很容易实现对Memcached进行访问,但是这离我们缓存页面的目标还差很多,因为有两个大问题还没有解决。

  • 首先,我们没有用到Memcached的分布式,我们上面的示例只是对一个Memcached的节点进行访问。
  • 其次,通过这种方式返回的页面的没有携带浏览器缓存相关的响应头。没有这些响应头,页面就不能缓存在浏览器了,会导致每次访问都会去请求服务器。

使Ngxin支持Memcached的分布式访问

Nginx可以通过upstream支持访问多个Memcached服务节点:


upstream memcached { server 127.0.0.1:11211; server 127.0.0.1:11212; server 127.0.0.1:11213; server 127.0.0.1:11214; } server { listen 80; server_name dev.hwtrip.com; location ^~ /cache/ { set $memcached_key $request_uri; memcached_pass memcached; } error_page 404 502 504 = @fallback; } ......

但是,upstream采用的是round-robin的轮询方式,而我们知道PHP的php_memcache扩展使用的是一致性哈希算法进行Memcached服务节点的选择。于是乎,我们在前端用PHP缓存的页面,通过nginx不一定能访问到。所以我们必须让Nginx也能通过一致性哈希算法去选择节点。

这里我们用到了ngx_http_upstream_consistent_hash这个第三方模块,这个模块实现了跟php_memcache这个PHP扩展一样的一致性哈希算法。

重新编译Nginx,添加好这个模块,我们修改下Nginx的配置文件的upstream部分:

upstream memcached {
    consistent_hash $request_uri;
    server 127.0.0.1:11211;
    server 127.0.0.1:11212;
    server 127.0.0.1:11213;
    server 127.0.0.1:11214;
}

......

我们修改下缓存页面的示例PHP代码:

$htmlContent = file_get_contents('http://youlun.hwtrip.com');

$memcached = new Memcache();
$memcached->addServer('127.0.0.1', 11211);
$memcached->addServer('127.0.0.1', 11212);
$memcached->addServer('127.0.0.1', 11213);
$memcached->addServer('127.0.0.1', 11214);

for ($i = 1; $i < 10; $i ++) { 
    $cacheIns->set("/cache/index$i.html", $htmlContent);
}

通过设置不同的key,我们测试下Nginx是否能获取到正确的内容。经测试,PHP设置的缓存,用Nginx都能正常访问到。

返回浏览器可缓存的页面

这是第一个例子里Nginx返回的页面的响应头:

Nginx

可以看到没有返回任何缓存相关的响应头,这样每次访问,浏览器都会去请求服务器,虽然服务器有缓存,但明显不符合我们对性能优化的追求。不过就算Nginx返回了相关的响应头,然后我们请求的时候包含了If-Modified-Since这个请求头,Ngxin的memcached模块也不会去判断这个请求有没有过期以及返回304 Not Modified。所以我们需要实现两件事,第一是能让Ngxin返回正确的响应头,第二是能让Nginx判断请求的资源是否过期,并正确返回响应码。

这里,我们借助另外一个Nginx的第三方模块:gx_http_enhanced_memcached_module。这个模块提供了很多功能,大家可以到它的github页面上了解。这里我们主要用到它的两个功能:

  • Send custom http headers, like Content-Type, Last-Modified. Http headers are stored in memcached, with your body data.
  • Reply 304 Not Modified for request with If-Modified-Since headers and content with Last-Modified in cache
    再次重新编译安装Nginx,添加好gx_http_enhanced_memcached_module模块,我们再次对Nginx的配置文件进行修改:
upstream memcached {
    consistent_hash $request_uri;
    server 127.0.0.1:11211;
    server 127.0.0.1:11212;
    server 127.0.0.1:11213;
    server 127.0.0.1:11214;
}

server {
    listen       80;
    server_name  dev.hwtrip.com;

    location ^~ /cache/ {
        set                     $enhanced_memcached_key $request_uri;
        enhanced_memcached_pass memcached;
    }

    error_page     404 502 504 = @fallback;
}

......

我们再次修改示例PHP文件:

$htmlContent = file_get_contents('http://youlun.hwtrip.com');

// 页面过期时间
$expiresTime = 60 * 5;

// Last-Modified头设置的时间
$lastModified = gmdate('D, d M Y H:i:s GMT', time());

// Expires头设置的时间
$expires = gmdate('D, d M Y H:i:s GMT', time() + $expiresTime);

// 最终缓存的内容
$cacheContent = "EXTRACT_HEADERS
Content-Type: text/html
Cache-Control:max-age=$expiresTime
Expires:$expires
Last-Modified:$lastModified

$htmlContent";

// 获取memcache实例
$memcached = new Memcache();
$memcached->addServer('127.0.0.1', 11211);
$memcached->addServer('127.0.0.1', 11212);
$memcached->addServer('127.0.0.1', 11213);
$memcached->addServer('127.0.0.1', 11214);

// 写入缓存
$memcached->set('/cache/index.html', $cacheContent, $expiresTime);

这次我们设置了缓存的过期时间,并在缓存内容前面添加了一些响应头。ngx_http_enhanced_memcached_module模块EXTRACT_HEADERS这个标记去识别并记录响应头,详情请看github页面的说明。

重新写入缓存后,我们再次访问页面:

Nginx

可以看到缓存相关的响应头都已正确返回。

后话

至此,我们已经简单的完成了使用Nginx和Memcached对缓存页面的访问,但这只是后端的简单实现,在前端还需要实现对页面缓存的管理等等的工作。

使用nginx limit_req限制用户请求速率

在《Nginx使用limit_rate limit_conn限制文件下载速度》我们说到了ngx_http_limit_conn_module 模块,来限制连接数。那么请求数的限制该怎么做呢?这就需要通过ngx_http_limit_req_module 模块来实现,该模块可以通过定义的 键值来限制请求处理的频率。特别的,可以限制来自单个IP地址的请求处理频率。 限制的方法如同漏斗,每秒固定处理请求数,推迟过多请求。

ngx_http_limit_req_module模块指令

limit_req_zone

  • 语法: limit_req_zone $variable zone=name:size rate=rate;
  • 默认值: none
  • 配置段: http
    设置一块共享内存限制域用来保存键值的状态参数。 特别是保存了当前超出请求的数量。 键的值就是指定的变量(空值不会被计算)。如
limit_req_zone $binary_remote_addr zone=one:10m rate=1r/s;

说明:区域名称为one,大小为10m,平均处理的请求频率不能超过每秒一次。
键值是客户端IP。
使用$binary_remote_addr变量, 可以将每条状态记录的大小减少到64个字节,这样1M的内存可以保存大约1万6千个64字节的记录。
如果限制域的存储空间耗尽了,对于后续所有请求,服务器都会返回 503 (Service Temporarily Unavailable)错误。
速度可以设置为每秒处理请求数和每分钟处理请求数,其值必须是整数,所以如果你需要指定每秒处理少于1个的请求,2秒处理一个请求,可以使用 “30r/m”。

limit_req_log_level

  • 语法: limit_req_log_level info | notice | warn | error;
  • 默认值: limit_req_log_level error;
  • 配置段: http, server, location
    设置你所希望的日志级别,当服务器因为频率过高拒绝或者延迟处理请求时可以记下相应级别的日志。 延迟记录的日志级别比拒绝的低一个级别;比如, 如果设置“limit_req_log_level notice”, 延迟的日志就是info级别。

limit_req_status

  • 语法: limit_req_status code;
  • 默认值: limit_req_status 503;
  • 配置段: http, server, location
    该指令在1.3.15版本引入。设置拒绝请求的响应状态码。

limit_req

  • 语法: limit_req zone=name [burst=number] [nodelay];
  • 默认值: —
  • 配置段: http, server, location
    设置对应的共享内存限制域和允许被处理的最大请求数阈值。 如果请求的频率超过了限制域配置的值,请求处理会被延迟,所以所有的请求都是以定义的频率被处理的。 超过频率限制的请求会被延迟,直到被延迟的请求数超过了定义的阈值,这时,这个请求会被终止,并返回503 (Service Temporarily Unavailable) 错误。这个阈值的默认值为0。如:
limit_req_zone $binary_remote_addr zone=ttlsa_com:10m rate=1r/s;
server {
    location /www.ttlsa.com/ {
        limit_req zone=ttlsa_com burst=5;
    }
}

限制平均每秒不超过一个请求,同时允许超过频率限制的请求数不多于5个。
如果不希望超过的请求被延迟,可以用nodelay参数,如:

limit_req zone=ttlsa_com burst=5 nodelay;

完整实例配置

http {
    limit_req_zone $binary_remote_addr zone=ttlsa_com:10m rate=1r/s;

    server {
        location  ^~ /download/ {  
            limit_req zone=ttlsa_com burst=5;
            alias /data/www.ttlsa.com/download/;
        }
    }
}

可能要对某些IP不做限制,需要使用到白名单。名单设置参见后续的文档,我会整理一份以供读者参考。请专注。

使用nginx image_filter 缩放,裁剪和生成缩略图

背景

项目中有个地方需要根据客户端的要求缩放图片。最开始想用PHP来实现这个功能。设想中如果已经存在图片a.jpg,则可以通过类似a_400x400.jpg的方式来获取图片特定尺寸的缩略图。

要实现此功能可以在图片上传的时候就事先裁好指定尺寸的图片,或者在获取的时候拦截请求来实现。

如果使用第一种方法,则只能实现裁剪好预设尺寸的图片,而且会影响到上传图片的效率,如果裁剪失败,也无法后续处理。

使用第二种方式的问题是图片资源存储在一个静态资源的目录,需要在没有图片的情况下将请求转发给PHP去处理。

于是我设想能否在Nginx这一层去做这件事情,恰好Nginx有一个image filter的模块,只不过在编译的时候默认没有编译进去。

手动添加参数编译此模块,开始修改nginx的配置文件。

配置

第一个版本的配置如下:

# 我使用16进制数的方式给图片重命名
location ~* /(.*)/([0-9a-f]+)_(d+)x(d+).(jpg|png|jpeg|gif)$ {
    # 如果存在文件就终止规则
    if (-f $request_filename) {
        break;
    }

    # 设定一些参数
    set $filepath $1;
    set $filename "$2.$5";
    set $thumb    "$2_$3x$4.$5";
    set $width    $3;
    set $height   $4;

    # 如果原文件不存在可以直接返回404
    if (!-f $document_root/$filepath/$filename) {
        return 404;
    }

    # 重写URL
    rewrite /(.*)/([0-9a-f]+)_([0-9x]+).(jpg|png|jpeg|gif) /$1/$2.$4 break;
    # 执行图片缩放
    image_filter test;
    image_filter resize $width $height;
    image_filter_jpeg_quality 75;
}

但是在这个版本的配置中,如果配置原文件不存在,实际上没法正确返回404,而是返回415。过滤还是执行了。

还有一个问题就是在每次访问缩略图的时候都会重新生成,如果访问量比较大的情况下,效率并不高。

进过一系列的实践后,我又改好了一个版本:

location ~* /(.*)/([0-9a-f]+)_(d+)x(d+).(jpg|png|jpeg|gif)$ {
    if (-f $request_filename) {
        break;
    }

    set $filepath $1;
    set $filename "$2.$5";
    set $thumb    "$2_$3x$4.$5";
    set $width    $3;
    set $height   $4;

    if (!-f $document_root/$filepath/$filename) {
        return 404;
    }

    rewrite /(.*)/([0-9a-fx_]+).(.*) /imgcache/$2.$3;

    if (!-f $request_filename) {
        proxy_pass http://127.0.0.1:$server_port/image-resize/$filepath/$filename?width=$width&height=$height;
        break;
    }

    proxy_store          $document_root/imgcache/$thumb;
    proxy_store_access   user:rw  group:rw  all:r;
    proxy_set_header     Host $host;
}

location /image-resize {
    rewrite /(image-resize)/(.*) /$2 break;

    image_filter resize $arg_width $arg_height;
    image_filter_jpeg_quality 75;

    allow 127.0.0.0/8;
    deny all;
}

通过proxy_pass的方式解决415的问题,并通过proxy_store的方式将图片存到指定的目录(imgcache),在每次访问之前先进行判断。

开启nginx的gzip压缩功能,节省流量

开启网站的 gzip 压缩功能,通常可以高达70%,也就是说,如果你的网页有30K,压缩之后就变成9K, 对于大部分网站,显然可以明显提高浏览速度(注:需要浏览器支持)。

nodejs + express

对于 nodejs + express 框架,启用方法非常简单,启用 compress() 中间件即可, 通过 gzip / deflate 压缩响应数据,这个中间件应该放置在所有的中间件最前面以保证所有的返回都是被压缩的。

代码如下:

app.use(express.logger());
app.use(express.compress());
app.use(express.static(__dirname + '/html'));
app.use(express.methodOverride());
app.use(express.bodyParser());

nginx

编辑 nginx 的配置文件

sudo vi /etc/nginx/nginx.conf

在 Gzip Settings 中加入如下设置:

##
# Gzip Settings
##

gzip on;
gzip_min_length 1k;
gzip_buffers 4 16k;
gzip_comp_level 5;
gzip_types text/plain application/x-javascript text/css application/xml text/javascript application/x-httpd-php;

1) gzip

  • 语法:gzip on/off
  • 默认值:off
  • 作用域:http, server, location
  • 说明:开启或者关闭 gzip 模块,这里使用 on 表示启动

2) gzip_min_length

  • 语法:gzip_min_length length
  • 默认值:gzip_min_length 0
  • 作用域:http, server, location
  • 说明:设置允许压缩的页面最小字节数,页面字节数从header头中的Content-Length中进行获取。默认值是0,不管页面多大都压缩。建议设置成大于1k的字节数,小于1k可能会越压越大。|

3) gzip_buffers

  • 语法: gzip_buffers number size
  • 默认值: gzip_buffers 4 4k/8k
  • 作用域: http, server, location
  • 说明:设置系统获取几个单位的缓存用于存储gzip的压缩结果数据流。4 16k 代表以 16k 为单位,按照原始数据大小以 16k 为单位的4倍申请内存。

4) gzip_comp_level

  • 语法: gzip_comp_level 1..9
  • 默认值: gzip_comp_level 1
  • 作用域: http, server, location
  • 说明:gzip压缩比,1 压缩比最小处理速度最快,9 压缩比最大但处理最慢(传输快但比较消耗cpu)。这里设置为 5。

5) gzip_types

  • 语法: gzip_types mime-type [mime-type …]
  • 默认值: gzip_types text/html
  • 作用域: http, server, location
  • 说明:匹配MIME类型进行压缩,(无论是否指定)”text/html” 类型总是会被压缩的。这里设置为 text/plain application/x-javascript text/css application/xml text/javascript application/x-httpd-php。

使用docker运行nginx

方法一、通过 Dockerfile构建

创建Dockerfile
首先,创建目录nginx,用于存放后面的相关东西。

runoob@runoob:~$ mkdir -p ~/nginx/www ~/nginx/logs ~/nginx/conf

www目录将映射为nginx容器配置的虚拟目录
logs目录将映射为nginx容器的日志目录
conf目录里的配置文件将映射为nginx容器的配置文件
进入创建的nginx目录,创建Dockerfile

FROM debian:jessie

MAINTAINER NGINX Docker Maintainers "[email protected]"

ENV NGINX_VERSION 1.10.1-1~jessie

RUN apt-key adv --keyserver hkp://pgp.mit.edu:80 --recv-keys 573BFD6B3D8FBC641079A6ABABF5BD827BD9BF62 
        && echo "deb http://nginx.org/packages/debian/ jessie nginx" >> /etc/apt/sources.list 
        && apt-get update 
        && apt-get install --no-install-recommends --no-install-suggests -y 
                                                ca-certificates 
                                                nginx=${NGINX_VERSION} 
                                                nginx-module-xslt 
                                                nginx-module-geoip 
                                                nginx-module-image-filter 
                                                nginx-module-perl 
                                                nginx-module-njs 
                                                gettext-base 
        && rm -rf /var/lib/apt/lists/*

# forward request and error logs to docker log collector
RUN ln -sf /dev/stdout /var/log/nginx/access.log 
        && ln -sf /dev/stderr /var/log/nginx/error.log

EXPOSE 80 443

CMD ["nginx", "-g", "daemon off;"]

通过Dockerfile创建一个镜像,替换成你自己的名字

docker build -t nginx .

创建完成后,我们可以在本地的镜像列表里查找到刚刚创建的镜像

runoob@runoob:~/nginx$ docker images nginx
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
nginx               latest              555bbd91e13c        3 days ago          182.8 MB

方法二、docker pull nginx

查找Docker Hub上的nginx镜像

runoob@runoob:~/nginx$ docker search nginx
NAME                      DESCRIPTION                                     STARS     OFFICIAL   AUTOMATED
nginx                     Official build of Nginx.                        3260      [OK]       
jwilder/nginx-proxy       Automated Nginx reverse proxy for docker c...   674                  [OK]
richarvey/nginx-php-fpm   Container running Nginx + PHP-FPM capable ...   207                  [OK]
million12/nginx-php       Nginx + PHP-FPM 5.5, 5.6, 7.0 (NG), CentOS...   67                   [OK]
maxexcloo/nginx-php       Docker framework container with Nginx and ...   57                   [OK]
webdevops/php-nginx       Nginx with PHP-FPM                              39                   [OK]
h3nrik/nginx-ldap         NGINX web server with LDAP/AD, SSL and pro...   27                   [OK]
bitnami/nginx             Bitnami nginx Docker Image                      19                   [OK]
maxexcloo/nginx           Docker framework container with Nginx inst...   7                    [OK]
...

这里我们拉取官方的镜像

runoob@runoob:~/nginx$ docker pull nginx

等待下载完成后,我们就可以在本地镜像列表里查到REPOSITORY为nginx的镜像。
使用nginx镜像
运行容器

runoob@runoob:~/nginx$ docker run -p 80:80 --name mynginx -v $PWD/www:/www -v $PWD/conf/nginx.conf:/etc/nginx/nginx.conf -v $PWD/logs:/wwwlogs  -d nginx  
45c89fab0bf9ad643bc7ab571f3ccd65379b844498f54a7c8a4e7ca1dc3a2c1e
runoob@runoob:~/nginx$

命令说明:
-p 80:80:将容器的80端口映射到主机的80端口
–name mynginx:将容器命名为mynginx
-v $PWD/www:/www:将主机中当前目录下的www挂载到容器的/www
-v $PWD/conf/nginx.conf:/etc/nginx/nginx.conf:将主机中当前目录下的nginx.conf挂载到容器的/etc/nginx/nginx.conf
-v $PWD/logs:/wwwlogs:将主机中当前目录下的logs挂载到容器的/wwwlogs
查看容器启动情况

runoob@runoob:~/nginx$ docker ps
CONTAINER ID        IMAGE        COMMAND                      PORTS                         NAMES
45c89fab0bf9        nginx        "nginx -g 'daemon off"  ...  0.0.0.0:80->80/tcp, 443/tcp   mynginx
f2fa96138d71        tomcat       "catalina.sh run"       ...  0.0.0.0:81->8080/tcp          tomcat

通过浏览器访问
虚拟化技术

使用python MySQLdb操作mysql数据库

MySQLdb 是用于Python链接Mysql数据库的接口,它实现了 Python 数据库 API 规范 V2.0,基于 MySQL C API 上建立的。

如何安装MySQLdb?

为了用DB-API编写MySQL脚本,必须确保已经安装了MySQL。复制以下代码,并执行:

#!/usr/bin/python
# -*- coding: UTF-8 -*-

import MySQLdb

如果执行后的输出结果如下所示,意味着你没有安装 MySQLdb 模块:

Traceback (most recent call last):
  File "test.py", line 3, in <module>
    import MySQLdb
ImportError: No module named MySQLdb

安装MySQLdb,请访问 http://sourceforge.net/projects/mysql-python ,(Linux平台可以访问:https://pypi.python.org/pypi/MySQL-python)从这里可选择适合您的平台的安装包,分为预编译的二进制文件和源代码安装包。
如果您选择二进制文件发行版本的话,安装过程基本安装提示即可完成。如果从源代码进行安装的话,则需要切换到MySQLdb发行版本的顶级目录,并键入下列命令:

$ gunzip MySQL-python-1.2.2.tar.gz
$ tar -xvf MySQL-python-1.2.2.tar
$ cd MySQL-python-1.2.2
$ python setup.py build
$ python setup.py install

** 注意 ** :请确保您有root权限来安装上述模块。

数据库连接

连接数据库前,请先确认以下事项:
– 您已经创建了数据库 TESTDB.
– 在TESTDB数据库中您已经创建了表 EMPLOYEE
– EMPLOYEE表字段为 FIRST_NAME, LAST_NAME, AGE, SEX 和 INCOME。
– 连接数据库TESTDB使用的用户名为 “testuser” ,密码为 “test123”,你可以可以自己设定或者直接使用root用户名及其密码,Mysql数据库用户授权请使用Grant命令。
– 在你的机子上已经安装了 Python MySQLdb 模块。

实例:

以下实例链接Mysql的TESTDB数据库:

#!/usr/bin/python
# -*- coding: UTF-8 -*-

import MySQLdb

# 打开数据库连接
db = MySQLdb.connect("localhost","testuser","test123","TESTDB" )

# 使用cursor()方法获取操作游标 
cursor = db.cursor()

# 使用execute方法执行SQL语句
cursor.execute("SELECT VERSION()")

# 使用 fetchone() 方法获取一条数据库。
data = cursor.fetchone()

print "Database version : %s " % data

# 关闭数据库连接
db.close()

执行以上脚本输出结果如下:

Database version : 5.0.45

创建数据库表

如果数据库连接存在我们可以使用execute()方法来为数据库创建表,如下所示创建表EMPLOYEE:

#!/usr/bin/python
# -*- coding: UTF-8 -*-

import MySQLdb

# 打开数据库连接
db = MySQLdb.connect("localhost","testuser","test123","TESTDB" )

# 使用cursor()方法获取操作游标 
cursor = db.cursor()

# 如果数据表已经存在使用 execute() 方法删除表。
cursor.execute("DROP TABLE IF EXISTS EMPLOYEE")

# 创建数据表SQL语句
sql = """CREATE TABLE EMPLOYEE (
         FIRST_NAME  CHAR(20) NOT NULL,
         LAST_NAME  CHAR(20),
         AGE INT,  
         SEX CHAR(1),
         INCOME FLOAT )"""

cursor.execute(sql)

# 关闭数据库连接
db.close()

数据库插入操作

以下实例使用执行 SQL INSERT 语句向表 EMPLOYEE 插入记录:

#!/usr/bin/python
# -*- coding: UTF-8 -*-

import MySQLdb

# 打开数据库连接
db = MySQLdb.connect("localhost","testuser","test123","TESTDB" )

# 使用cursor()方法获取操作游标 
cursor = db.cursor()

# SQL 插入语句
sql = """INSERT INTO EMPLOYEE(FIRST_NAME,
         LAST_NAME, AGE, SEX, INCOME)
         VALUES ('Mac', 'Mohan', 20, 'M', 2000)"""
try:
   # 执行sql语句
   cursor.execute(sql)
   # 提交到数据库执行
   db.commit()
except:
   # Rollback in case there is any error
   db.rollback()

# 关闭数据库连接
db.close()

以上例子也可以写成如下形式:

#!/usr/bin/python
# -*- coding: UTF-8 -*-

import MySQLdb

# 打开数据库连接
db = MySQLdb.connect("localhost","testuser","test123","TESTDB" )

# 使用cursor()方法获取操作游标 
cursor = db.cursor()

# SQL 插入语句
sql = "INSERT INTO EMPLOYEE(FIRST_NAME, 
       LAST_NAME, AGE, SEX, INCOME) 
       VALUES ('%s', '%s', '%d', '%c', '%d' )" % 
       ('Mac', 'Mohan', 20, 'M', 2000)
try:
   # 执行sql语句
   cursor.execute(sql)
   # 提交到数据库执行
   db.commit()
except:
   # 发生错误时回滚
   db.rollback()

# 关闭数据库连接
db.close()

** 实例:**
以下代码使用变量向SQL语句中传递参数:

..................................
user_id = "test123"
password = "password"

con.execute('insert into Login values("%s", "%s")' % 
             (user_id, password))
..................................

数据库查询操作

Python查询Mysql使用 fetchone() 方法获取单条数据, 使用fetchall() 方法获取多条数据。
– fetchone(): 该方法获取下一个查询结果集。结果集是一个对象
– fetchall():接收全部的返回结果行.
– rowcount: 这是一个只读属性,并返回执行execute()方法后影响的行数。
** 实例:**
查询EMPLOYEE表中salary(工资)字段大于1000的所有数据:

#!/usr/bin/python
# -*- coding: UTF-8 -*-

import MySQLdb

# 打开数据库连接
db = MySQLdb.connect("localhost","testuser","test123","TESTDB" )

# 使用cursor()方法获取操作游标 
cursor = db.cursor()

# SQL 查询语句
sql = "SELECT * FROM EMPLOYEE 
       WHERE INCOME > '%d'" % (1000)
try:
   # 执行SQL语句
   cursor.execute(sql)
   # 获取所有记录列表
   results = cursor.fetchall()
   for row in results:
      fname = row[0]
      lname = row[1]
      age = row[2]
      sex = row[3]
      income = row[4]
      # 打印结果
      print "fname=%s,lname=%s,age=%d,sex=%s,income=%d" % 
             (fname, lname, age, sex, income )
except:
   print "Error: unable to fecth data"

# 关闭数据库连接
db.close()

以上脚本执行结果如下:

fname=Mac, lname=Mohan, age=20, sex=M, income=2000

数据库更新操作

更新操作用于更新数据表的的数据,以下实例将 EMPLOYEE 表中的 SEX 字段为 ‘M’ 的 AGE 字段递增 1:

#!/usr/bin/python
# -*- coding: UTF-8 -*-

import MySQLdb

# 打开数据库连接
db = MySQLdb.connect("localhost","testuser","test123","TESTDB" )

# 使用cursor()方法获取操作游标 
cursor = db.cursor()

# SQL 更新语句
sql = "UPDATE EMPLOYEE SET AGE = AGE + 1 WHERE SEX = '%c'" % ('M')
try:
   # 执行SQL语句
   cursor.execute(sql)
   # 提交到数据库执行
   db.commit()
except:
   # 发生错误时回滚
   db.rollback()

# 关闭数据库连接
db.close()

删除操作

删除操作用于删除数据表中的数据,以下实例演示了删除数据表 EMPLOYEE 中 AGE 大于 20 的所有数据:

#!/usr/bin/python
# -*- coding: UTF-8 -*-

import MySQLdb

# 打开数据库连接
db = MySQLdb.connect("localhost","testuser","test123","TESTDB" )

# 使用cursor()方法获取操作游标 
cursor = db.cursor()

# SQL 删除语句
sql = "DELETE FROM EMPLOYEE WHERE AGE > '%d'" % (20)
try:
   # 执行SQL语句
   cursor.execute(sql)
   # 提交修改
   db.commit()
except:
   # 发生错误时回滚
   db.rollback()

# 关闭连接
db.close()

执行事务

事务机制可以确保数据一致性。
事务应该具有4个属性:原子性、一致性、隔离性、持久性。这四个属性通常称为ACID特性。
– 原子性(atomicity)。一个事务是一个不可分割的工作单位,事务中包括的诸操作要么都做,要么都不做。
– 一致性(consistency)。事务必须是使数据库从一个一致性状态变到另一个一致性状态。一致性与原子性是密切相关的。
– 隔离性(isolation)。一个事务的执行不能被其他事务干扰。即一个事务内部的操作及使用的数据对并发的其他事务是隔离的,并发执行的各个事务之间不能互相干扰。
– 持久性(durability)。持续性也称永久性(permanence),指一个事务一旦提交,它对数据库中数据的改变就应该是永久性的。接下来的其他操作或故障不应该对其有任何影响。
Python DB API 2.0 的事务提供了两个方法 commit 或 rollback。
实例:

# SQL删除记录语句
sql = "DELETE FROM EMPLOYEE WHERE AGE > '%d'" % (20)
try:
   # 执行SQL语句
   cursor.execute(sql)
   # 向数据库提交
   db.commit()
except:
   # 发生错误时回滚
   db.rollback()

对于支持事务的数据库, 在Python数据库编程中,当游标建立之时,就自动开始了一个隐形的数据库事务。
commit()方法游标的所有更新操作,rollback()方法回滚当前游标的所有操作。每一个方法都开始了一个新的事务。
错误处理
DB API中定义了一些数据库操作的错误及异常,下表列出了这些错误和异常:

异常 描述
Warning 当有严重警告时触发,例如插入数据是被截断等等。必须是 StandardError 的子类。
Error 警告以外所有其他错误类。必须是 StandardError 的子类。
InterfaceError 当有数据库接口模块本身的错误(而不是数据库的错误)发生时触发。 必须是Error的子类。
DatabaseError 和数据库有关的错误发生时触发。 必须是Error的子类。
DataError 当有数据处理时的错误发生时触发,例如:除零错误,数据超范围等等。 必须是DatabaseError的子类。
OperationalError 指非用户控制的,而是操作数据库时发生的错误。例如:连接意外断开、 数据库名未找到、事务处理失败、内存分配错误等等操作数据库是发生的错误。 必须是DatabaseError的子类。
IntegrityError 完整性相关的错误,例如外键检查失败等。必须是DatabaseError子类。
InternalError 数据库的内部错误,例如游标(cursor)失效了、事务同步失败等等。 必须是DatabaseError子类。
ProgrammingError 程序错误,例如数据表(table)没找到或已存在、SQL语句语法错误、 参数数量错误等等。必须是DatabaseError的子类。
NotSupportedError 不支持错误,指使用了数据库不支持的函数或API等。例如在连接对象上 使用.rollback()函数,然而数据库并不支持事务或者事务已关闭。 必须是DatabaseError的子类。

zabbix3监控mysql性能状态

如果被监控的主机上已经安装好了mysql,在安装zabbix-agent的时候就会自动创建一个userparameter_mysql.conf

配置zabbix目录

vim /etc/zabbix/zabbix_agentd.conf
#取消注释,没有则创建一个
Include=/etc/zabbix/zabbix_agentd.d/

修改mysql监控的的配置

vim /etc/zabbix/zabbix_agentd.d/userparameter_mysql.conf

UserParameter=mysql.status[*],echo "show global status where Variable_name='$1';" | HOME=/var/lib/zabbix mysql -N | awk '{print $$2}'

UserParameter=mysql.size[*],echo "select sum($(case "$3" in both|"") echo "data_length+index_length";; data|index) echo "$3_length";; free) echo "data_free";; esac)) from information_schema.tables$([[ "$1" = "all" || ! "$1" ]] || echo " where table_schema='$1'")$([[ "$2" = "all" || ! "$2" ]] || echo "and table_name='$2'");" | HOME=/var/lib/zabbix mysql -N

UserParameter=mysql.ping,HOME=/var/lib/zabbix mysqladmin ping | grep -c alive

UserParameter=mysql.version,mysql -V

修改my.cnf 配置

# Zabbix Agent ;注意sock文件路径
[mysql]
host     = localhost
user     = zabbix
password = 密码
socket   = /var/run/mysqld/mysqld.sock
[mysqladmin]
host     = localhost
user     = zabbix
password = 密码
socket   = /var/run/mysqld/mysqld.sock

为被监控的主机添加mysql模版

添加mysql监控的模版

监控

查看获取的mysql数据

监控

zabbix自动发现并监控本机的多memcached实例

本人在工作中一般喜欢把MySQL、Redis、Memcached、MongoDB等数据库按照实例的方式对外提供服务。一般都是一台高配的服务器上开启多个实例给每个业务使用。而监控是重中之重,我自己也尝试了多种监控方式,但对我来说感觉最简单最快的就是使用zabbix了,灵活定义key。

由于我是多实例,所以就需要用到zabbix的自动发现功能(LLD)。基本处理方式就是:

  • 1、写自动发现脚本。

  • 2、写状态取值脚本。

  • 3、添加配置文件。

  • 4、添加权限。

  • 5、配置zabbix web。

一、写自动发现脚本

$ cat /etc/zabbix/zabbix_agentd.d/scripts/memcached_discovery.py
#!/usr/bin/env python
import os
import json
t=os.popen("""sudo netstat -nltp|awk -F: '/memcached/&&/LISTEN/{print $2}'|awk '{print $1}'| grep -v grep | grep -v '^$'   """)
ports = []
for port in  t.readlines():
        r = os.path.basename(port.strip())
        ports += [{'{#MCPORT}':r}]
print json.dumps({'data':ports},sort_keys=True,indent=4,separators=(',',':'))

执行脚本看输出结果(最好使用zabbix用户执行,才能看出效果):

$ python /etc/zabbix/zabbix_agentd.d/scripts/memcached_discovery.py
{
    "data":[
        {
            "{#MCPORT}":"11211"
        },
        {
            "{#MCPORT}":"11212"
        }
}

我这个脚本中使用了sudo权限,zabbix用户在执行netstat时需要sudo权限。

二、写状态取值脚本

#!/bin/bash
#
#Auth: Pengdongwen
#Blog: www.ywnds.com
#Email: [email protected]
#Desc: memcached status monitoring 
#dependent:
#  1)yum install nc
#  2)python memcached_discovery.py
#########################

IP=127.0.0.1 
PORT="$1"
METRIC="$2"

if [ $# -lt 2 ];then
    echo "please set argument"
    exit 1
fi

STATUS=`echo "stats" | nc $IP $PORT | grep -w "$METRIC" | awk '{print $3}'`
case $METRIC in
    'version')
        echo $STATUS
        ;;
    'uptime')
        echo $STATUS 
        ;;
    'curr_connections')
        echo $STATUS
        ;;
    'total_connections')
        echo $STATUS
        ;;
    'cmd_get')
        echo $STATUS
        ;;
    'cmd_set')
        echo $STATUS
        ;;
    'get_hits')
        echo $STATUS
        ;;
    'get_misses')
        echo $STATUS
        ;;
    'bytes_read')
        echo $STATUS
        ;;
    'bytes_written')
        echo $STATUS
        ;;
    'curr_items')
        echo $STATUS
        ;;
    'total_items')
        echo $STATUS
        ;;
    'expired_unfetched')
        echo $STATUS
        ;;
    'evicted_unfetched')
        echo $STATUS
        ;;
    *)
        echo "Not selected metric"
        exit 0
        ;;
esac

脚本很简单,需要传给脚本两个参数,一个是端口号,另一个是监控值。

有几个特别需要说明的就是:

  • 1)这个脚本不支持redis加密。

  • 2)需要指定redis-cli的绝对路径。

  • 3)需要安装dos2unix工具(yum install dos2unix)。

三、添加配置文件

$ cat /etc/zabbix/zabbix_agentd.d/userparameter_memcached.conf
UserParameter=memcached.discovery[*],python /etc/zabbix/zabbix_agentd.d/scripts/memcached_discovery.py
UserParameter=memcached[*],/bin/bash /etc/zabbix/zabbix_agentd.d/scripts/memcached_status.sh $1 $2

这里定义三个key,第一个key是用于自动发现的。第二个key是用于取不同实例的状态值的,传了两个参数,$1是端口号(从自动发现中获取的),第二个是传的参数。端口号和参数我会在zabbix页面配置传给memcached[*]这个key。

都配置完后就可以添加重启一下zabbix-agent了。

$ service zabbix-agent restart

四、添加权限

需要给zabbix用户添加sudo权限。

$ cat /etc/sudoers.d/zabbix
Defaults:zabbix    !requiretty
zabbix ALL=(ALL) NOPASSWD: SUPERVISORCTLZB
Cmnd_Alias SUPERVISORCTLZB = /sbin/ss,/usr/sbin/ss,/sbin/dmidecode,/usr/sbin/dmidecode,/sbin/service,/usr/sbin/service,/bin/netstat

另外需要注意的是,普通用户zabbix默认环境变量有如下这些:

$ echo $PATH
/usr/local/bin:/bin:/usr/bin:/usr/local/sbin:/usr/sbin

所以你要确认你所有的执行程序都在这些路径下,不然zabbix是获取不到值的。

使用zabbix用户执行看是否正常。

$ sudo -u zabbix `which zabbix_agentd` -t redis.discovery[*]
{
    "data":[
        {
            "{#MCPORT}":"11211"
        },
        {
            "{#MCPORT}":"11212"
        }
}

五、配置zabbix web

前期工作都做完了,下面就可以配置zabbix web了。

首先创建一个模板(Template Linux Memcached Discovery),然后在模板中创建一个自动发现规则(Linux Memcached Discovery)。

监控

在这个自动发现规则内创建一个item。

监控

然后可以创建trigger等。

下面我提供一个模板Github:https://github.com/dongwenpeng/zabbix

centos 7 yum安装配置apache 2.4

本文介绍在centos 7 yum安装apache 2.4,即通过yum仓库来快速安装已经编译好的版本,这样比源码编译省下不少时间。

安装和配置apache

1.安装apache 2.4

sudo yum install httpd

2.更新httpd.conf中的网站根目录配置。添加区块来配置资源使用。
文件片断:/etc/httpd/conf/httpd.conf

DocumentRoot "/var/www/html/example.com/public_html"

...

<IfModule prefork.c>
    StartServers        5
    MinSpareServers     20
    MaxSpareServers     40
    MaxRequestWorkers   256
    MaxConnectionsPerChild 5500
</IfModule>

配置基于名称的虚拟主机

1.在conf.d目录创建vhost.conf来存储你的虚拟主机配置文件。下面是example.com网站的模板;根据你的需要来更改:
文件片断:/etc/httpd/conf.d/vhost.conf

NameVirtualHost *:80

<VirtualHost *:80>
    ServerAdmin [email protected]
    ServerName example.com
    ServerAlias www.example.com
    DocumentRoot /var/www/html/example.com/public_html/
    ErrorLog /var/www/html/example.com/logs/error.log
    CustomLog /var/www/html/example.com/logs/access.log combined
</VirtualHost>

其它额外的域名也同样添加到vhost.conf文件。要添加域名,复制上面的VirtualHost区块并更改。当收到来自互联网的一个新的请求,apache将在vhost.conf来检查域名匹配的VirtualHost区块:
Apache

2.创建与上面相关的目录

sudo mkdir -p /var/www/html/example.com/{public_html,logs}

3.设置apache开机启动并重启服务:

sudo systemctl enable httpd.service
sudo systemctl restart httpd.service

现在可以访问你的域名来测试apache server了。如果在你的网站根目录没有找到index文件,将显示默认的页面
Apache

配置firewalld来允许web流量

CentOS 7内置的防火墙默认设置阻止web流量。执行如下命令来允许web流量:

sudo firewall-cmd --add-service=http --permanent && sudo firewall-cmd --add-service=https --permanent 
sudo systemctl restart firewalld