Redis 缓存雪崩,击穿,穿透的区别

  • 缓存雪崩

如果所有页面的key失效时间都是12小时,中午12点刷新,0点有个秒杀活动大量用户涌入,假设当时每秒6000个请求,本来缓存在可以扛住每秒5000个请求,但是缓存当时所有的key都失效了,此时所有的请求都落到了db上,此时db扛不住,一批缓存key同时失效,导致请求到db层,这种情况就是缓存雪崩。

缓存雪崩–key全部失效或大面积失效

雪崩如何避免呢?Redis存数据的时候,每个key 失效时间加一个随机值,这样可以保证数据不会在同一时刻大面积失效。

setRedis(Key,value,time + Math.random() * 10000);
  • 缓存穿透

缓存穿透是指缓存和数据库都没有的数据,而用户不断发起请求,比如数据库id都是从1开始自增的,此时用户(攻击者)发起一个id为-1的数据或id特别大不存在的数据,攻击会导致数据库压力过大,而这种穿过缓存的情况,就叫缓存穿透。

缓存穿透

这种可以通过校验非法值,来防止缓存穿透的情况

  • 缓存击穿

缓存击穿与缓存雪崩有点像,但是不一样的是:缓存雪崩是因为大面积的缓存失效,打到db上,而缓存击穿是指某一个key非常热点,在不停地扛着大并发,大并发集中对这一点进行访问,当这个key在失效的瞬间,持续并发,就击穿缓存,直接请求db了。

缓存穿透的预防上面已经说明了,可以在接口层增加校验,比如用户鉴权,参数做校验,不合法的参数直接代码return,比如id基础校验,范围校验等。

缓存击穿如何防呢?可以设置热点数据永不过期,或者加上互斥锁就能搞定了。

缓存击穿的预防

php自定义扩展开发

文章源于: https://www.bo56.com/php7%E6%89%A9%E5%B1%95%E5%BC%80%E5%8F%91%E4%B9%8Bhello-word/ ,经亲自验证,此文章代码只适用于php7

我们来生成一个say扩展,say扩展提供say方法,方法功能是返回hello world字符串

  • 利用ext_skel 生成扩展骨架
cd /data/src/php-7.2.25/ext //进入php源码扩展目录
./ext_skel --extname=say

extname参数的值就是扩展名称。执行ext_skel命令后,这样在当前目录下会生成一个与扩展名一样的目录。

  • 修改config.m4配置文件

config.m4的作用就是配合phpize工具生成configure文件。configure文件是用于环境检测的。检测扩展编译运行所需的环境是否满足。现在我们开始修改config.m4文件。

 $ cd ./say
 $ vim ./config.m4
dnl If your extension references something external, use with:
   
dnl PHP_ARG_WITH(say, for say support,
dnl Make sure that the comment is aligned:
dnl [  --with-say             Include say support])
 
dnl Otherwise use enable:
 
dnl PHP_ARG_ENABLE(say, whether to enable say support,
dnl Make sure that the comment is aligned:
dnl [  --enable-say           Enable say support])

其中,dnl 是注释符号。上面的代码说,如果你所编写的扩展如果依赖其它的扩展或者lib库,需要去掉PHP_ARG_WITH相关代码的注释。否则,去掉 PHP_ARG_ENABLE 相关代码段的注释。我们编写的扩展不需要依赖其他的扩展和lib库。因此,我们去掉PHP_ARG_ENABLE前面的注释。去掉注释后的代码如下:

dnl If your extension references something external, use with:
    
 dnl PHP_ARG_WITH(say, for say support,
 dnl Make sure that the comment is aligned:
 dnl [  --with-say             Include say support])
  
 dnl Otherwise use enable:
  
 PHP_ARG_ENABLE(say, whether to enable say support,
 Make sure that the comment is aligned:
 [  --enable-say           Enable say support])
  • 代码实现

修改say.c文件。实现say方法。
找到PHP_FUNCTION(confirm_say_compiled),在其上面增加如下代码:

PHP_FUNCTION(say)
{
        zend_string *strg;
        strg = strpprintf(0, "hello word");
        RETURN_STR(strg);
}

找到 PHP_FE(confirm_say_compiled, 在上面增加如下代码:

PHP_FE(say, NULL)

修改后的代码如下:

const zend_function_entry say_functions[] = {
     PHP_FE(say, NULL)       /* For testing, remove later. */
     PHP_FE(confirm_say_compiled,    NULL)       /* For testing, remove later. */
     PHP_FE_END  /* Must be the last line in say_functions[] */
};
  • 编译安装
/usr/local/php-7.2.25/bin/phpize
./configure --with-php-config=/usr/local/php-7.2.25/bin/php-config
make
make install

//进入扩展目录
-rwxr-xr-x 1 root root 151K Apr 14 11:32 mcrypt.so
-rwxr-xr-x 1 root root 3.5M Apr 14 11:21 opcache.a
-rwxr-xr-x 1 root root 1.9M Apr 14 11:21 opcache.so
-rwxr-xr-x 1 root root 104K Apr 14 11:28 pcntl.so
-rwxr-xr-x 1 root root 1.7M Apr 14 12:05 redis.so
-rwxr-xr-x 1 root root  31K May 31 10:14 say.so //自定义的扩展
  • 编辑php.ini文件
extension="say.so"
  • 命令行测试扩展
[root@ip /usr/local/php/lib]# /usr/local/php/bin/php -r "echo say();"
hello word[root@ip /usr/local/php/lib]# 

linux 共享库编译介绍

  • 基础使用
gcc -fPIC -c a.c
gcc -fPIC -c b.c
gcc -shared -Wl -o libmyab.so ao b.o
// -fPIC 表示位置无关代码,动态库是运行时加载,PIC表示库放到内存任何地址上运行都行,用的是相对地址,而不是绝对地址
// -c 只编译,生成.o文件,不进行链接
  • 高阶使用
gcc -share -Wl,-soname, libmyab.so.1 -o libmyab.so.1.0.1 a.o b.o
gcc -share -Wl,-soname, your_soname -o library_name(real_name) file_list library_list
  • 基本概念

按照共享库的命名惯例,每个共享库有三个文件名:real name, soname和linker name,这三个name 有什么区别呢?

一般真正的库文件的名字叫real name, 包含了完整的共享库版本号
soname 是一个符号链接的名字,只包含共享库的主版本号,主版本号一致即可保证库函数的接口一致,因此应用程序的.dynamic段只记录共享库的soname,只要soname一致,这个共享库就可以用。表示库函数接口没有大的变化。如libmyab.so.1和libmyab.so.2是两个主版本号不同的libmyab,有些应用依赖于libmyab.so.1,有些应用依赖于libmyab.so.2,但是对于依赖libmyab.so.1的应用程序来说,真正的库文件不管是libmyab.so.1.10还是libmyab.so.1.11都可以用。所以使用共享库可以很方便地升级库文件而不需要重新编译应用程序,这是静态库所没有的优点。注意libc的版本编号有一点特殊,libc-2.8.90.so的主版本号是6而不是2或是2.8

linker name 仅在编译链接时使用,gcc的-L选项应该指定linker name所在的目录。有的linker name是库文件的一个符号链接,有的linker name是一段链接脚本。

centos /lib64下

举例说:上面libxml2.so 这个就是linker name,linkxml2.so.2,这个是soname, 标识了主版本号,libxml2.so.2.9.2这个是real name, 给出了完整的版本号

共享库加载:

在所有基于GNUglibc的系统中,在启动一个ELF二进制执行程序时,一个特殊的程序“程序装载器”会被自动装载并运行。在linux中,这个程序装载器就是/lib/ld-linux.so.X(X是版本号)。它会查找并装载应用程序所依赖的所有共享库。被搜索的目录保存在/etc/ld.so.conf文件中。如果程序的每次启动,都要去搜索一番,性能上会有所消耗。linux系统已经考虑到这一点。对共享库采用缓存管理,ldconfig就是实现这一功能的工具,其缺省读取/etc/ld.so.conf文件,对所有的共享库按照一定规范建立符号连接,然后将信息写入/etc/ld.so.cache。/etc/ld.so.cache的存在大大地加快了程序的启动速度

下面我们用一个例子【编译一个加减乘除的运算so】来详细说明下共享库(so->shared object)的编译过程:

//我的目录结构:
|-- add.c
|-- cmath.h
|-- dive.c
|-- main.c
|-- mul.c
|-- sub.c

//我的程序代码
//add.c
int add(int a, int b) {
	return a+b;
}

//sub.c
int sub(int a, int b) {
	return a-b;
}

//mul.c
int mul(int a, int b) {
	return a*b;
}

//dive.c
int dive(int a, int b) {
	return a/b;
}

//cmath.h
#ifndef CMATH_H_
#define CMATH_H_
int add(int a, int b);
int sub(int a, int b);
int mul(int a, int b);
int dive(int a, int b);
#endif

//main.c
#include <stdio.h>
#include "cmath.h"

int main(void) {
	printf("%d", add(3,4));
	return 0;
}
  • 编译产生.o文件
gcc -fPIC -c *.c

//执行后的目录结构,会产生四个.0文件
|-- add.c
|-- add.o
|-- cmath.h
|-- dive.c
|-- dive.o
|-- main.c
|-- main.o
|-- mul.c
|-- mul.o
|-- sub.c
`-- sub.o
  • 将.o编译生成共享库
gcc -shared -Wl,-soname,libmym.so.1 -o libmysm.so.1.10 *.o
//执行完的目录结构,会产生一个libmym.so.1.10
|-- add.c
|-- add.o
|-- app
|-- cmath.h
|-- dive.c
|-- dive.o
|-- libmysm.so.1.10
|-- main.c
|-- main.o
|-- mul.c
|-- mul.o
|-- sub.c
`-- sub.o
  • 将main,c 和 libmym.so.1.10一起编译,生成app程序
gcc main.c libmysm.so.1.10 -o app

  • 我们来执行下./app
./app: error while loading shared libraries: libmym.so.1: cannot open shared object file: No such file or directory

说找不到so,我们接着ldd查看下app所依赖的so

[root@VM_16_5_centos src]# ldd app 
	linux-vdso.so.1 =>  (0x00007ffccebeb000)
	libmym.so.1 => not found
	libc.so.6 => /usr/lib64/libc.so.6 (0x00007f9d5bcf7000)
	/lib64/ld-linux-x86-64.so.2 (0x00007f9d5c0c4000)

这个时候我们来聊到一个文件 /etc/ld.so.conf

include ld.so.conf.d/*.conf
/usr/local/lib64
/usr/local/lib
/usr/lib
/usr/lib64
/root/ccode/src

ld.so.conf 存放的是一个so搜索目录的文件列表,可以简单理解成类似path环境,这里面存放的so的环境path

由于是自定义的so. 我们的libmym.so.1.10存放在/root/ccode/src下,所以我们把这个路径加到ld.so.conf里

然后用下列命令更新下共享库路径:

ldconfig -v

再次运行下app程序

[root@VM_16_5_centos src]# ldd app 
	linux-vdso.so.1 =>  (0x00007ffcae9e1000)
	libmym.so.1 => /root/ccode/src/libmym.so.1 (0x00007f8390595000)
	libc.so.6 => /usr/lib64/libc.so.6 (0x00007f83901c8000)
	/lib64/ld-linux-x86-64.so.2 (0x00007f8390797000)
[root@VM_16_5_centos src]# ./app 
7[root@VM_16_5_centos src]# 

sshpass使用详解

sshpass主要的作用是可以在命令行直接使用密码来进行远程操作(远程连接和远程拉取文件)或是免密免交互进行某些本地操作,之所以会聊到这个命令,今天在迁移发布打包脚本的时候整理查资料总结的

使用前提,需要安装sshpass:

//yum安装
yum -y install sshpass

//源码安装
tar xvzf sshpass-1.05.tar.gz 
cd sshpass-1.05.tar.gz 
./configure 
make 
make install 

实例1:

sshpass -p 密码 ssh -oStrictHostKeyChecking=no -p 端口 root@ip

如果不加-oStrictHostKeyChecking=no,是无法远程登录的.

StrictHostKeyChecking: 主机公钥确认
(1)StrictHostKeyChecking=no 最不安全的级别,当然也没有那么多烦人的提示了,相对安全的内网测试时建议使用。如果连接server的key在本地不存在,那么就自动添加到文件中(默认是known_hosts),并且给出一个警告。
(2)StrictHostKeyChecking=ask 默认的级别,就是出现刚才的提示了。如果连接和key不匹配,给出提示,并拒绝登录。
(3)StrictHostKeyChecking=yes 最安全的级别,如果连接与key不匹配,就拒绝连接,不会提示详细信息。

实例1:可以这样测试没有StrictHostKeyCheck为什么不行:

ssh p 端口 root@ip

会发现,原来需要登录确认,所以实例1指令如果后面的命令不带-oStrictHostKeyChecking就没反应:

[root@xxxxx ~]# ssh -p 端口 root@ip
The authenticity of host '[ip]:端口 ([ip]:端口)' can't be established.
ECDSA key fingerprint is cc:d8:89:91:b2:b9:56:4d:0b:d7:e3:c7:be:a4:c1:50.
Are you sure you want to continue connecting (yes/no)? yes
Warning: Permanently added '[ip]:端口' (ECDSA) to the list of known hosts.
root@ip's password:

当输入密码后,会往~/.ssh/know_hosts里加一条记录(表示信任),后续则不再需要check了.

除了上述ssh -o加上StrictHostKeyChecking=no之外,还可以用下面的方法免交互确认

~/.ssh/config (或者在/etc/ssh/ssh_config)
Host *(或是指定的host)
  StrictHostKeyChecking no

               

实例2:在本地执行远程机器的命令

sshpass -p xxx ssh root@192.168.11.11 "ethtool eth0"

[root@xxxx ~]# sshpass -p 密码 ssh -oStrictHostKeyChecking=no -p 端口号 root@ip
Last login: Thu May 28 23:50:49 2020 from ip2
Welcome to tlinux 2.2 64bit
[root@yy ~]# cd /tmp/
[root@yyy /tmp]# ls -lh | grep clark
-rw-r--r-- 1 root        root           0 May 28 23:51 clarksss.log

                   

实例3:从密码文件读取内容作为密码去远程连接主机

sshpass -f xxx.txt  ssh root@192.168.11.11

fork和exec函数初探

一个进程,包括代码、数据和分析给进程的资源。fork()函数通过系统调用创建一个与原来进程几乎完全相同的进程,也就是两个进程可以做完全相同的事,但如果初始参数或传入变量不同,两个进程也可以做不同的事。

一个进程调用fork函数后,系统先给新的进程分配资源,例如存储数据和代码的空间。然后把原来进程的所有值都复制到新的进程中,只有少数值与原来进程不同,相当于克隆了一个自己。

看一个简单的例子:

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

int main(int ac, char *av[]) {
	pid_t pid;
	int count = 0;

	printf("current process id = %d\n", getpid());

	if ((pid = fork()) < 0) {
		printf("异常退出");
		exit(1);
	} else if (pid == 0) {
		printf("pid:%d\n", pid);
		count++;
		printf("count:%d\n", count);
		printf("进入子进程,当前进程currentPid =%d, 父进程id: %d\n", getpid(), getppid());
	} else {
		sleep(3); //如果没有这一句,父进程会先跑完,然后子进程的父进程号就不是我们的预期了,而可能是-1
		printf("pid:%d\n", pid);
		count++;
		printf("count:%d\n", count);
		printf("我是父进程,当前进程currentPid=%d, 子进程id: %d\n", getpid(), pid);
	}
	return 0;
}

在语句pid = fork()之前,只有一个进程在执行这段代码,但是在这条语句之后,就变成了两个进程在执行了,这两个进程几乎完全相同,将要执行的下一条语句是”else if (pid == 0)”

两个进程的pid 不同,fork调用一次,却能返回两次,有三种不同的返回值:

  • 父进程中,fork返回新创建子进程的进程id.
  • 在子进程中,fork返回0
  • 如果出现错误,fork返回一个负值

fork出错可能有两种原因:

  • 当前的进程数已经达到系统规定的上限,这是errno的值被设置EAGAIN
  • 系统内存不足,这是errno的值被设置为ENOMEM

创建新进程后,系统中出现两个基本完全相同的进程。这两个进程执行没有固定的先后顺序,哪个进程先执行要看系统的进程调度策略,所以上述代码中,父进程中给了sleep,否则父进程先于子进程结束,子进程里获取的父进程号无法符合预期的父进程号,而是得到-1

进程调用fork,当控制转移到内核中的fork代码后,内核做:

  • 分配新的内存块和内核数据结构
  • 复制原来的进程到新的进程
  • 向运行进程集添加新的进程
  • 将控制返回给两个进程

exec函数族

#include <unistd.h>

int execl(const char *path, const char *arg, ...);
int execle(const char *path, const char *arg, ..., char *const envp[]);
int execv(const char *path, char *const argv[]);
int execve(const char *path, char *const argv[], char *const envp[]);

int execlp(const char *file, const char *arg, ...);
int execvp(const char *file, char *const argv[]);

exec函数族的功能是根据指定的文件名找到可执行文件执行它并用它来取代调用进程的内容,即取代原调用进程的数据段、代码段和堆栈段。

当exec函数正常执行时,原调用进程的内容除了pid是保留的,其他的全部被新进程替换了。且除非exec函数调用失败,否则exec函数后的所有代码都不会再被执行。

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

main() {
	char *arglist[3];
	arglist[0] = "ls";
	arglist[1] = "-l";
	arglist[2] = NULL;
	printf("*** About to exec ls -l\n");
	execvp("ls", arglist);
	printf("*** ls is done bye\n");
}

结果就是执行ls -l命令,并且不会打印 ls is done bye这一行,因为execvp后面的所有代码都不会再被执行。

那通过exec 的学习,其实会想到shell,shell本身是一个程序或是一个进程,然后当我们执行某个指令后,他会将原进程用新进程替代。下面我们来用execvp实现一个简单shell程序:

#include <stdio.h>
#include <signal.h>
#include <string.h>
#include <stdlib.h>

#define MAXARGS 20
#define ARGLEN 100

int main() {
	char *arglist[MAXARGS + 1];
	int numargs;
	char argbuf[ARGLEN];
	char *makestring();

	numargs = 0;
	while (numargs < MAXARGS) {
		printf("Arg[%d]?", numargs);
		if (fgets(argbuf, ARGLEN, stdin) && *argbuf != '\n') {
			arglist[numargs++] = makestring(argbuf);
		} else {
			if (numargs > 0) {
				arglist[numargs] = NULL;
				execute(arglist);
				numargs = 0;
			}
		}
	}
	return 0;
}

int execute(char *arglist[]) {
	execvp(arglist[0], arglist);
	perror("execvp failed");
	exit(1);
}

char * makestring(char *buf) {
	void *cp = NULL;
	buf[strlen(buf) - 1] = '\0';
	cp = malloc(strlen(buf) + 1);
	if (cp == NULL) {
		fprintf(stderr, "no memory\n");
		exit(1);
	}
	strcpy(cp, buf);
	return cp;
}
//执行结果
[root@VM_16_5_centos ccode]# ./psh1
Arg[0]?netstat
Arg[1]?-an
Arg[2]?
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address           Foreign Address         State      
tcp        0      0 0.0.0.0:111             0.0.0.0:*               LISTEN     
tcp        0      0 0.0.0.0:80              0.0.0.0:*               LISTEN     
tcp        0      0 127.0.0.1:8089          0.0.0.0:*               LISTEN     
tcp        0      0 0.0.0.0:443             0.0.0.0:*               LISTEN     
tcp        0      0 0.0.0.0:36000           0.0.0.0:*               LISTEN     
tcp        0      0 127.0.0.1:45930         127.0.0.1:3306          TIME_WAIT 
tcp        0      0 127.0.0.1:45898         127.0.0.1:3306          TIME_WAIT 

以上就是fork和exec函数族里execvp的应用,exec函数族里还有其他几个函数,用法也类似,带p的,就是PATH, 带v的就是参数接受一个数组。功能和用法大体相同。

job告警定位全过程

最近两天领了一个活,定位项目中一个job的告警原因。
任务背景:这个job是每晚定时运行,job大体功能是多进程分片(最大同时有8个进程)去循环连接各个分库(库的数量数以万计),进入分库后,再进行两层循环的计算(总共三层循环),此job运行五分钟之后,总会时不时报,找不到分库数据源(但是实际上去db中查找,有些是脏的分库真不存在,但是有的数据源确实真存在)

定位方法&过程:

  • 既然告警当中确有不存在的数据源,会不会是其中一个分片进程循环遇到找不到的数据源而引起后面的连接出错的连锁反应呢?
    于是决定写一个脚本,把所有确不存在的脏数据源给理出来,然后统一在外层过滤掉,使得每个分片逻辑中不会去连脏的数据源
  • 当我完成上一步清理掉脏数据源后,再次跑了下这个job, 这次依旧是跑了五分钟后抛出几百封告警邮件,随机抽查了下,这次告警连不上的数据源都是存在的
    那么问题出在哪呢?只能详细去查代码定位了 🙁
<?php
	//伪代码如下
        ...
        //如果缓存数据源池里存在此数据源名,则直接从中取
	if ($name存在于缓存的连接池中) {
	        $conn = 缓存连接池里取出传入的连接名的数据库配置;
	} elseif (属于指定环境) {
	        $conn = 重新从数据源配置表里读取配置($name);
	} else {
	        $conn = null;
	}
	if (empty($conn)) {
	        抛异常("数据源名:" . $name...);
	        ....
	}
	...
?>

根据报错,是走进了empty conn逻辑,然后抛异常出来。那么问题来了,上面有if, elseif, else三个分支逻辑,究竟走到哪个分支逻辑,使得conn为空呢,目测的话,有可能走进elseif ,也有可能是走过了else逻辑,为了先确认这个问题,我把代码改了一下:

<?php
	//伪代码如下
        ...
        //如果缓存数据源池里存在此数据源名,则直接从中取
	if ($name存在于缓存的连接池中) {
                $flag = 1;
	        $conn = 缓存连接池里取出传入的连接名的数据库配置;
	} elseif (属于指定环境) {
                $flag = 2;
	        $conn = 重新从数据源配置表里读取配置($name);
	} else {
                $flag = 3;
	        $conn = null;
	}
	if (empty($conn)) {
            //之所以在这里重定向,而不在上面分支逻辑中埋log, 是因为上面的逻辑if elseif是正常逻辑,所有的正常业务,也可能会走到,会产生不必要的log
            error_log("empty conn, flag:" . $flag, 3, "/tmp/empty_conn.log");
	        抛异常("数据源名:" . $name...);
	        ....
	}
	...
?>

按照上面的代码,就知道走哪个分支逻辑使得conn变量为空了,这样便可以进一步知道null的来源,结论:定位到是else逻辑“$conn = 重新从数据源配置表里读取配置($name);”导致conn为空的。

紧接着查看“ 重新从数据源配置表里读取配置 ”这个方法,此方法主要是根据name传参查出数据源。

在此方法中再次埋了下log, 在mysqli_connect连接后打印了mysqli_errorno和mysqli_error()
mysqli_error()返回的信息是: “Cannot assign requested address“,其实这里面埋log 也一波三折,具体过程就不细聊了。

下面来看一下”Cannot assgin requested address”的原因分析:

“Cannot assign requested address.”是由于linux分配的客户端连接端口用尽,无法建立socket连接所致,虽然socket正常关闭,但是端口不是立即释放,而是处于TIME_WAIT状态,默认等待60s后才释放。为什么会用尽呢?通常这个情况出现在高并发数据库又是短连接,端口迅速被分配完,虽然代码层有mysql close 但是端口并不会快速被释放,所以就出现了此问题。

如何解决, 网上查了下解决方案:

解决方法1–调低time_wait状态端口等待时间:

  1. 调低端口释放后的等待时间,默认为60s,修改为15~30s
    sysctl -w net.ipv4.tcp_fin_timeout=30
  2. 修改tcp/ip协议配置, 通过配置/proc/sys/net/ipv4/tcp_tw_resue, 默认为0,修改为1,释放TIME_WAIT端口给新连接使用
    sysctl -w net.ipv4.tcp_timestamps=1
  3. 修改tcp/ip协议配置,快速回收socket资源,默认为0,修改为1
    sysctl -w net.ipv4.tcp_tw_recycle=1

解决办法2–增加可用端口:

shell> $ sysctl -a | grep port_range
net.ipv4.ip_local_port_range = 50000    65000      -----意味着50000~65000端口可用 

修改参数:

$ vi /etc/sysctl.conf
net.ipv4.ip_local_port_range = 10000     65000      -----意味着10000~65000端口可用

改完后,执行命令“sysctl -p”使参数生效,不需要reboot

但是我没有按照上面的方案来优化,原因主要是:
我再次跑了下此job, 告警的时候,我观察了下netstat -an | grep TIME_WAIT | wc -l, 结果一度飙到6w多,所以爆了,job一度告警,不仅这个job, 其他同时间的job也受到牵连,根据经验,闲时的并发量,一般达不到这个值。代码应该写的有些许问题。但是目前不想去改代码,因为改代码:一,需要时间;二,需要验证。

我做了如下几点优化:

  • 查数据库源配置信息的方法,之前每次循环查一次就连一次db,我将连db这里做成了单例,第二次循环之后,用的是前一次连接句柄。 接着观察了下db的tcp连接数,已经较之前下降了一半, 但是按照上面的修改,仍然不能满足要求,试了下单纯只跑这个job 虽然不会告警,但是如果同一时间有其他job也在跑的话,很可能还会告警,TIME_WAIT仍然会很高。
  • 降低此job总的并发进程数
  • 限制此job在每台job机上的运行个数

通过上面三个优化手段之后,再次跑了下此job,并且是模拟正常job量的情况下运行的,发现已经没有告警了。当然后续可以对此脚本再优化,也是能起到一定效果的。

再次查资料查了下TIME_WAIT相关资料:

TCP状态转换(这是从网上找的一张图)

一眼看过去,这么多状态,各个方向的连线,但是整个图可以被划分为三个部分,上半部是建立连接的过程,左下部是主动关闭连接,右下部是被动关闭连接的过程。

从上图可以看出来,当TCP连接主动关闭时,会经过TIME_WAIT状态,所以TIME_WAIT状态是TCP四次握手结束后,连接双方都不再交换消息,但主动关闭的一方保持这个连接一段时间内不可用。

那这个TIME_WAIT状态有什么用呢?

暂以 A、B 来代指 TCP 连接的两端,A 为主动关闭的一端。

四次挥手中,A 发 FIN, B 响应 ACK,B 再发 FIN,A 响应 ACK 实现连接的关闭。而如果 A 响应的 ACK 包丢失,B 会以为 A 没有收到自己的关闭请求,然后会重试向 A 再发 FIN 包。

如果没有 TIME_WAIT 状态,A 不再保存这个连接的信息,收到一个不存在的连接的包,A 会响应 RST 包,导致 B 端异常响应。

此时, TIME_WAIT 是为了保证全双工的 TCP 连接正常终止

允许老的重复分节在网络中消逝:

我们还知道,TCP 下的 IP 层协议是无法保证包传输的先后顺序的。如果双方挥手之后,一个网络四元组(src/dst ip/port)被回收,而此时网络中还有一个迟到的数据包没有被 B 接收,A 应用程序又立刻使用了同样的四元组再创建了一个新的连接后,这个迟到的数据包才到达 B,那么这个数据包就会让 B 以为是 A 刚发过来的。

此时, TIME_WAIT 的存在是为了保证网络中迷失的数据包正常过期。

那TIME_WAIT的时长是多少呢?

TIME_WAIT的过期时间,MSL(最大分段寿命,Maximum Segment Lifetime),它表示一个TCP分段可以存在于互联网系统中的最大时间,由TCP的实现,超出这个寿命的分片就会被丢弃。

TIME_WAIT 状态由主动关闭的 A 来保持,那么我们来考虑对于 A 来说,可能接到上一个连接的数据包的最大时长:A 刚发出的数据包,能保持 MSL 时长的寿命,它到了 B 端后,B 端由于关闭连接了,会响应 RST 包,这个 RST 包最长也会在 MSL 时长后到达 A,那么 A 端只要保持 TIME_WAIT 到达 2MSL 就能保证网络中这个连接的包都会消失。

MSL 的时长被 RFC 定义为 2分钟,但在不同的 unix 实现上,这个值不并确定,我们常用的 centOS 上,它被定义为 30s,我们可以通过 /proc/sys/net/ipv4/tcp_fin_timeout 这个文件查看和修改这个值。2MSL就是60s.

大量TIME_WAIT造成的影响:

在高并发短连接的TCP服务器上,当服务器处理完请求后立刻主动正常关闭连接。这个场景下会出现大量socket处于TIME_WAIT状态。如果客户端的并发量持续很高,此时部分客户端就会显示连接不上。
主动正常关闭TCP连接,都会出现TIMEWAIT。

为什么我们要关注这个高并发短连接呢?有两个方面需要注意:

  1. 高并发可以让服务器在短时间范围内同时占用大量端口,而端口有个0~65535的范围,并不是很多,刨除系统和其他服务要用的,剩下的就更少了。
  2. 在这个场景中,短连接表示”业务处理+传输数据的时间 远远小于 TIMEWAIT超时的时间”的连接。

这里有个相对长短的概念,比如取一个web页面,1秒钟的http短连接处理完业务,在关闭连接之后,这个业务用过的端口会停留在TIMEWAIT状态几分钟,而这几分钟,其他HTTP请求来临的时候是无法占用此端口。

linux c cp程序实现

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>

#define BUFFERSIZE 4096
#define COPYMODE 0644

void oops(char *, char *);

main(int ac, char *av[]) {
	int in_fd, out_fd, n_chars;
	char buf[BUFFERSIZE];

	if (ac !=3) {
		fprintf(stderr, "usage: %s source destination\n", *av);
		exit(1);
	}

	if ((in_fd = open(av[1], O_RDONLY)) == -1) {
		oops("Cannot open", av[1]);
	}

	if ((out_fd = creat(av[2], COPYMODE)) == -1) {
		oops("Cannot creat", av[2]);
	}

	while((n_chars = read(in_fd, buf, BUFFERSIZE)) > 0) {
		if (write(out_fd, buf, n_chars) != n_chars) {
			oops("Write error to", av[2]);
		}
	}

	if (n_chars == -1) {
		oops("Read error from", av[1]);
	}

	if (close(in_fd) == -1 || close(out_fd) == -1) {
		oops("Error closing files", "");
	}

}

void oops(char *s1, char * s2) {
	fprintf(stderr, "Error: %s ", s1);
	perror(s2);
	exit(1);
}
$ cc cp1.c -o cp1
$ cp1 src dest

总结:

创建/重写文件,如果文件不存在,就创建它,存在,就把它的内容清空,把文件的长度设为0
creat
作用:创建/重写一个文件
头文件:fcntl.h
原型:int fd = creat(char *filename, mode_t mode)
参数:filename 文件名,mode 访问模式
返回值:-1 遇到错误,fd成功创建

写文件:用write调用向已打开的文件中写入数据,如果写入失败,返回-1, 如果写入成功,返回写入的字节数
write
目标:将内存中的数据写入文件
函数原型:ssize_t result = write(int fd, void *buf, size_t amt)
参数:fd 文件描述符;buf 内存数据,amt 要写的字节数
返回值:-1 遇到错误;num written 成功写入

读文件:由已打开的文件读数据,返回值为实际读取到的字节数,如果返回0,表示已到达文件尾或是无可读取的数据,此外文件读写位置会随读取到的字节移动。目的会把参数fd所指的文件传送count个字节到buf指针所指的内存中
read
头文件:unistd.h
函数原型: ssize_t read(int fd,void * buf ,size_t count); 
参数:fd 文件描述符,buf 读取的内容存放处, count 读取的字节数

main
int argc (传递的参数个数,文件名也算是一个)
char *argv[] 传递的参数值

为什么系统调用需要很多时间?

用户进程位于用户空间,内核位于系统空间,磁盘只能被内核直接访问。上述程序cp1要读取磁盘上的数据只能通过系统调用read,而read代码在内核中,所以当read调用发生时,执行权会从用户代码转移到内核代码,执行内核代码是需要时间的。

系统调用的开销大不仅仅是因为要传输数据,当运行内核代码时,cpu工作在管理员模式,这对应于一些特殊的堆栈和内存环境,必须在系统调用 发生时建立好。系统调用结束后(read返回时),cpu又要切换到用户模式,必须把堆栈和内存环境恢复到用户程序运行的状态,这种运行环境的切换要消耗很多时间。

当工作在管理员模式下,程序可以直接访问磁盘,终端,打印机等设备,还可以访问全部的内存空间,而用户模式,程序不能直接访问设备,也只能访问特定部分的内存空间。在运行时刻,系统会根据需要不断地在两种模式间切换。管理员模式和用户模式的切换与cpu关系很大,cpu中特定的标记来区分当前的工作模式,而unix系统的设计必须考虑到cpu的这种特点,才能实现不同工作模式间的良好切换。

在计算机的世界中也是一样,要是cpu把太多的时间消耗在执行内核代码和模式切换上,就不可能有很多时间来执行程序中业务逻辑的代码或提供系统服务,所以要尽可能地减少模式间的切换,对于系统来说,这种时间上的开销是昂贵的。

shell 脚本getopts用法总结

最近在写一个消费者的发布脚本,用到getopts,所以总结下getopts的用法

getopts是shell命令行参数解析工具,意在从shell 命令行当中解析参数。命令格式:

getopts optstring name [arg ...]

optstring列出了对应的shell 脚本可以识别的所有参数。比如:shell script可以识别-a, -f 以及-s参数,则optstring就是afs;如果对应的参数后面还跟随一个值,则在相应的optstring后面加冒号比如a:fs表示a参数后面会有一个值出现,-a value的形式另外,getopts执行匹配到a的时候,会把value存放在一个OPTARG的shell 变量中。如果optstring是以冒号开头的,命令行当中出现了optstring当中没有的参数将不会提示错误信息。

name表示的是参数的名称,每次执行getopts,会从命令行当中获取下一个参数,然后存放到name中。如果获取到参数不在optstring中,则name的值被设置为?。命令行当中的所有参数都有一个index,第一个参数从1开始,依次类推,另外有一个名为OPTIND的shell变量存放下一个要处理的参数的index。

直接上例子:

#!/bin/bash
 
func() {
    echo "Usage:"
    echo "test.sh [-j S_DIR] [-m D_DIR]"
    echo "Description:"
    echo "S_DIR,the path of source."
    echo "D_DIR,the path of destination."
    exit -1
}
 
upload="false"
 
while getopts 'h:j:m:u' OPT; do
    case $OPT in
        j) S_DIR="$OPTARG";;
        m) D_DIR="$OPTARG";;
        u) upload="true";;
        h) func;;
        ?) func;;
    esac
done
 
echo $S_DIR
echo $D_DIR
echo $upload

//执行结果:
[root@bobo tmp] sh test.sh -j /data/usw/web -m /opt/data/web
/data/usw/web
/opt/data/web
false
 
[root@bobo tmp] sh test.sh -j /data/usw/web -m /opt/data/web -u
/data/usw/web
/opt/data/web
true
 
[root@bobo tmp] sh test.sh -j /data/usw/web
/data/usw/web
 
false
 
[root@bobo tmp] sh test.sh -m /opt/data/web                 
 
/opt/data/web
false
 
[root@bobo tmp] sh test.sh -h
test.sh: option requires an argument -- h
Usage:
test.sh [-j S_DIR] [-m D_DIR]
Description:
S_DIR,the path of source.
D_DIR,the path of destination.
 
[root@bobo tmp] sh test.sh j
 
 
false
 
[root@bobo tmp] sh test.sh j m
 
 
false

getopts后面跟的字符串就是参数列表,每个字母代表一个选项,如果字母后面跟一个冒号,则表示这个选项还会有一个值,比如-j /data/usr/web 和 -m /opt/data/web。而getopts字符串中没有跟随冒号的字母就是开关型的选项,不需要指定值,等同于true/false,只要带上这个参数就是true。

shift位移使用举例:

#!/bin/bash
#test3.sh

func() {
        echo "Usage:"
        echo "test.sh [-j S_DIR] [-m D_DIR]"
        echo "Description:"
        echo "S_DIR, the path of source."
        echo "D_DIR, the path of destination."
        exit -1
}

upload="false"

echo $OPTIND

while getopts 'h:s:d:u' OPT; do
        case $OPT in
                s) S_DIR="$OPTARG";;
                d) D_DIR="$OPTARG";;
                u) upload="true";;
                h) func;;
                ?) func;;
        esac
done

echo $OPTIND
echo $(($OPTIND -))
echo $1

//执行结果:
[root@bobo tmp] sh test3.sh -s /data/usw/web beijing
1              #执行的是第一个"echo $OPTIND"
3              #执行的是第二个"echo $OPTIND"
beijing        #此时$1是"beijing"
 
[root@bobo tmp] sh test3.sh -s /opt/data/web beijing                
1              #执行的是第一个"echo $OPTIND"
3              #执行的是第二个"echo $OPTIND"
beijing
 
[root@bobo tmp] sh test3.sh -s /data/usw/web -d /opt/data/web beijing
1              #执行的是第一个"echo $OPTIND"
5              #执行的是第二个"echo $OPTIND"
beijing
 
                  参数位置: 1        2       3       4        5     6
[root@bobo tmp] sh test3.sh -s /data/usw/web -d /opt/data/web -u beijing
6
beijing

mysql binlog详解

文章内容源于MySQL 8 Cookbook【这本书不错】

二进制日志包含数据库的所有更改记录,包括数据和结构两方面。二进制日志不记录SELECT 或 SHOW 等不修改数据的操作。运行带有二进制日志的服务器会带来轻微的性能影响。二进制日志能保证数据库出故障时数据是安全的。只有完整的事件或事务会被记录或回读。

1.首先第一个问题:为什么应该使用二进制日志?

  • 复制:使用二进制日志,可以把对服务器所做的更改以流式方式传输到另一台服务器上。从(slave)服务器充当镜像副本,也可用于分配负载。接受写入的服务器称为主(master)服务器。
  • 时间点恢复:假设你在星期日的00:00进行了备份,而数据库在星期日的08:00出现故障。使用备份可以恢复到周日00:00的状态;而使用二进制日志可以恢复到周日08:00的状态。

2.启用binlog方法

要启用binlog,必须设置log_bin和server_id并重启服务器

例如log_bin设置为/data/mysql/binlogs/server1,二进制日志存储在/data/mysql/binlogs文件夹中名为server1.000001、server1.000002等日志文件中。每当服务器启动或刷新日志时,或者当前日志的大小达到max_binlog_size时,服务器都会在系列中创建一个新文件。每个二进制日志的位置都在server1.index文件中被维护

启用binlog代码如下:

shell> sudo vim /etc/my.cnf
[mysqld]
log_bin = /data/mysql/binlogs/server1
server_id = 100

如果log_bin不赋予任何值,在这种情况下,二进制日志是在数据目录中创建的。可以使用主机名作为目录名称

设置了log_bin,如果要生效,记得重启mysql

验证是否生效:

mysql> show variables like 'log_bin%';
log_bin                        On
log_bin_basename               /data/mysql/binlogs/server1
log_bin_index                  /data/mysql/binlogs/server1.index
......

执行SHOW BINARY LOGS;或SHOW MASTER LOGS;,以显示服务器的所有二进制日志

执行命令SHOW MASTER STATUS;以获取当前的二进制日志位置:

mysql> show master status;
File                Position
server1.000002       3273

一旦server1.00000x达到max_binlog_size(默认1G),一个新文件server1.00000x+1就会被创建,并被添加到server1.index中。可以动态设置max_binlog_size。

set @@global.max_binlog_size=xxxxxxxx

禁用会话的二进制日志

有些情况下我们不希望将执行语句复制到其他服务器上。为此,可以使用以下命令来禁用该会话的二进制日志:

mysql>set SQL_LOG_BIN = 0;

在这条语句后的所有SQL语句都不会被记录到二进制日志中,不过这仅仅是针对该会话的。要重新启用二进制日志,可以执行以下操作:

mysql>set SQL_LOG_BIN = 1;

移至下一个日志

可以使用FLUSH LOGS命令关闭当前的二进制日志并打开一个新的二进制日志:

mysql>flush logs;

清理二进制日志

随着写入次数的增多,二进制日志会消耗大量空间。如果放任不管,这些写入操作将很快占满磁盘空间,因此清理它们至关重要。1.使用binlog_expire_logs_seconds 和expire_logs_days 设置日志的到期时间。
如果想以天为单位设置到期时间,请设置 expire_logs_days。例如,如果要删除两天之前的所有二进制日志,请SET@@global.expire_logs_days=2。如果将该值设置为0,则禁用设置会自动到期。
如果想以更细的粒度来设置到期时间,可以使用binlog_expire_logs_seconds变量,它能够以秒为单位来设置二进制日志过期时间。
这个变量的效果和 expire_logs_days 的效果是叠加的。例如,如果expire_logs_days是1并且binlog_expire_logs_seconds是43200,那么二进制日志就会每 1.5 天清除一次。这与将binlog_expire_logs_seconds设置为129600、将expire_logs_days设置为0的效果是相同的。在 MySQL 8.0 中,binlog_expire_logs_seconds和expire_logs_days必须设置为0,以禁止自动清除二进制日志。

2.要手动清除日志,请执行PURGE BINARY LOGS TO ‘<file_name>’。例如,有server1.000001、server1.000002、server1.000003和server1.000004文件,如果执行 PURGE BINARY LOGS TO'server1.000004',则从server1.000001 到 server1.000003 的所有文件都会被删除,但文件server1.000004不会被删除:
除了指定某个日志文件,还可以执行命令PURGE BINARY LOGS BEFORE ‘2017-08-03 15:45:00’。除了使用BINARY,还可以使用MASTER。
mysql> PURGE MASTER LOGS TO ‘server1.000004’ 可以实现和之前语句一样的效果。

3.要删除所有二进制日志并再次从头开始,请执行RESET MASTER

binlog的格式

二进制日志可以写成下面三种格式。
1.STATEMENT:记录实际的SQL语句。
2.ROW:记录每行所做的更改。例如,更新语句更新10行,所有10行的更新信息都会被写入日志。而在基于语句的复制中,只有更新语句会被写入日志,默认格式是ROW。
3.MIXED:当需要时,MySQL会从STATEMENT切换到ROW。

有些语句在不同服务器上执行时可能会得到不同的结果。例如,UUID()函数的输出就因服务器而异。这些语句被称为非确定性的语句,基于这些语句的复制是不安全的。在这些情况下,当设置MIXED格式时,MySQL服务器会切换为基于行的格式。

可以使用兼具全局和会话范围作用域的动态变量binlog_format来设置格式。在全局范围进行设置可使所有客户端使用指定的格式:

mysql>set global binlog_format = 'statement';
mysql>set global binlog_format = 'row';

1.Statement:每一条会修改数据的sql都会记录在binlog中。

优点:不需要记录每一行的变化,减少了binlog日志量,节约了IO,提高性能。(相比row能节约多少性能与日志量,这个取决于应用的SQL情况,正常同一条记录修改或者插入row格式所产生的日志量还小于Statement产生的日志量,但是考虑到如果带条件的update操作,以及整表删除,alter表等操作,ROW格式会产生大量日志,因此在考虑是否使用ROW格式日志时应该跟据应用的实际情况,其所产生的日志量会增加多少,以及带来的IO性能问题。)

缺点:由于记录的只是执行语句,为了这些语句能在slave上正确运行,因此还必须记录每条语句在执行的时候的一些相关信息,以保证所有语句能在slave得到和在master端执行时候相同的结果。另外mysql 的复制,像一些特定函数功能,slave可与master上要保持一致会有很多相关问题(如sleep()函数, last_insert_id(),以及user-defined functions(udf)会出现问题).

使用以下函数的语句也无法被复制:

  • LOAD_FILE()
  • UUID()
  • USER()
  • FOUND_ROWS()
  • SYSDATE() (除非启动时启用了 –sysdate-is-now 选项)

同时在INSERT …SELECT 会产生比 RBR 更多的行级锁

2.Row:不记录sql语句上下文相关信息,仅保存哪条记录被修改。

优点: binlog中可以不记录执行的sql语句的上下文相关的信息,仅需要记录那一条记录被修改成什么了。所以rowlevel的日志内容会非常清楚的记录下每一行数据修改的细节。而且不会出现某些特定情况下的存储过程,或function,以及trigger的调用和触发无法被正确复制的问题

缺点:所有的执行的语句当记录到日志中的时候,都将以每行记录的修改来记录,这样可能会产生大量的日志内容,比如一条update语句,修改多条记录,则binlog中每一条修改都会有记录,这样造成binlog日志量会很大,特别是当执行alter table之类的语句的时候,由于表结构修改,每条记录都发生改变,那么该表每一条记录都会记录到日志中。

3.Mixedlevel: 是以上两种level的混合使用,一般的语句修改使用statment格式保存binlog,如一些函数,statement无法完成主从复制的操作,则采用row格式保存binlog,MySQL会根据执行的每一条具体的sql语句来区分对待记录的日志形式,也就是在Statement和Row之间选择一种.新版本的MySQL中队row level模式也被做了优化,并不是所有的修改都会以row level来记录,像遇到表结构变更的时候就会以statement模式来记录。至于update或者delete等修改数据的语句,还是会记录所有行的变更。

从二进制日志中提取语句:

使用mysqlbinlog程序可以从二进制日志中提取内容,并将其应用到其他服务器

实操:使用各种二进制格式执行几条语句。如果把binlog_format设置为GLOBAL级别(全局范围),必须断开并重新连接,以使更改生效。如果想保持连接,请把binlog_format设置为SESSION级别(会话范围)

更改为基于语句(statement)的的复制(SBR):

更改为基于行(Row)的复制(RBR):

更新几行:

改为mixed格式:

更新几行:

mysqlbinlog操作

要显示日志server1.000001的内容,请执行以下操作:

shell> mysqlbinlog /data/mysql/binlogs/server1.000001

你会得到类似下面的输出结果:

#at 206
#170815 12:49:24 server id 200 end log pos 312 CRC32 0x9197bf88 Query
thread_id exec_time=0 error_code=0
BINLOG '
~
~

在第一行中,#at后面的数字表示二进制日志文件中事件的起始位置(文件偏移量)。
第二行包含了语句在服务器上被启用的时间戳。
时间戳后面跟着 server ID、end_log_pos、thread_id、exec_time和error_code。

● server id:产生该事件的服务器的server_id值(在这个例子中为200)。
● end_log_pos:下一个事件的开始位置。● thread_id:指示哪个线程执行了该事件。
● exec_time:在主服务器上,它代表执行事件的时间;在从服务器上,它代表从服务器的最终执行时间与主服务器的开始执行时间之间的差值,这个差值可以作为备份相对于主服务器滞后多少的指标。
● error_code:代表执行事件的结果。零意味着没有错误发生。

回顾:

1.基于语句复制(SBR):我们在基于语句的复制中执行了UPDATE语句,而且在二进制日志中记录了相同的语句。除了保存在服务器上,会话变量也被保存在二进制日志中,以在从库上复制相同的行为:

2.当使用基于行的复制(RBR)时,会以二进制格式对整行(而不是语句)进行保存,而且二进制格式不能读取。此外,你可以观察长度,单个更新语句会生成很多数据。

3.当使用MIXED格式时,UPDATE语句被记录为SQL语句,而INSERT语句以基于行的格式被记录,因为INSERT有非确定性的UUID()函数:

提取的日志可以被传送给MySQL以回放事件。在重放二进制日志时最好使用force选项,这样即使force选项卡在某个点上,执行也不会停止。稍后,你可以查找错误并手动修复数据。

shell>mysqlbinlog /data/mysql/binlogs/server1.000001 | mysql -f -h <remote_host> -u <username> -p

或者也可以先保存到文件中,稍后执行:

shell> mysqlbinlog /data/mysql/binlogs/server1.000001 > server1.binlog_extract
shell> cat server1.binlog_extract | mysql -h <remote_user> -u <username> -p

根据时间和位置进行抽取我们可以通过指定位置从二进制日志中提取部分数据。假设你想做时间点恢复。假如在 2017-08-19 12:18:00 执行了 DROP DATABASE 命令,并且最新的可用备份是在2017-08-19 12:00:00 做的,该备份已经恢复。现在,需要恢复从 12:00:01 至2017-08-19 12:17:00 的数据。请记住,如果提取完整的日志,它还将包含 DROP DATABASE命令,该命令将再次擦除数据。可以通过–start-datetime和–stop-datatime选项来指定提取数据的时间窗口。

shell>mysqlbinlog /data/mysql/binlogs/server1.000001 --start-datetime="xxxx" --stop-datetime="yyyy" > binlog_extract

使用时间窗口的缺点是,你会失去灾难发生那一刻的事务。为避免这种情况,必须在二进制日志中使用事件的文件偏移量。一个连续的备份会保存它已完成备份的所有binlog文件的偏移量。备份恢复后,必须从备份的偏移量中提取binlog。我们将在第7章中详细了解备份。假设备份的偏移量为471,执行DROP DATABASE命令的偏移量为1793。可以使用–start-position和–stop-position选项来提取偏移量之间的日志:

shell>mysqlbinlog /data/mysql/binlogs/server.000001 --start-position=471 --stop-position=1793 > binlog_extract

请确保DROP DATABASE命令在提取的binlog中不再出现。基于数据库进行提取使用–database选项可以过滤特定数据库的事件。如果多次提交,则只有最后一个选项会被考虑。这对于基于行的复制非常有效。但对于基于语句的复制和MIXED,只有当选择默认数据库时才会提供输出。以下命令从employees 数据库中提取事件:

shell>mysqlbinlog /data/mysql/binlogs/server1.000001 --database=employees > binlog_extract

正如mysql 8文档中所解释的,假设二进制日志是通过使用基于语句的日志记录执行这些语句而创建的:

mysql>
INSERT INTO test.t1 (i) VALUES (100);
INSERT INTO db2.t2 (j) VALUES (200);

use test;
INSERT INTO test.t1(i) VALUES (101);
INSERT INTO t1(i) VALUES (102);
INSERT INTO db2.t2(j) VALUES (201);

use db2;
INSERT INTO test.t1(i) VALUES (103);
INSERT INTO db2.t2(j) VALUES (202);
INSERT INTO t2 (j) VALUES (203);

mysqlbinlog –database=test 不输出前两个 INSERT 语句,因为没有默认数据库。

mysqlbinlog –database=test 输出USE test后面的三条INSERT语句,但不是USE db2后面的三条INSERT语句。

因为没有默认数据库,mysqlbinlog –database=db2 不输出前两条INSERT语句。mysqlbinlog –database=db2不会输出USE test后的三条INSERT语句,但会输出在USE db2之后的三条INSERT语句。

提取行事件显示

默认情况下,基于行的复制日志显示为二进制格式。要查看行信息,必须将–verbose或-v选项传递给mysqlbinlog。行事件的二进制格式以注释的伪SQL语句的形式显示,其中的行以###开始。可以看到,单个更新语句被改写为了每行的UPDATE语句。

shell> mysqlbinlog /data/mysql/binlogs/server1.000001 --start-position=660 --stop-position=1298 -v
~
~
# at 660
#170815 13:29:02 server id 200 end_log_pos 722 CRC32 0xe0a2ec74
Table_map: `employees`.`salaries` mapped to number 165
# at 722
........
BINLOG *
二进制内容......
........
### update `employeeds`.`salaries`
### where
### @1=1001
### @2=240468
### @3='xxxxx'
### @4='zzzzzz'
###SET
### @1=1001
### @2=xxxx
.....

如果你只想查看没有二进制行信息的伪 SQL 语句,请指定–base64-output=”decode-rows”以及–verbose:

shell>mysqlbinlog /data/mysql/binlogs/server.00001 --start-position=xxx --stop-position=xxxx --verbose --base64-output="decode-rouws"

重写数据库名称

假设你想将生产服务器上的employees 数据库的二进制日志恢复为开发服务器上的employees_dev,可以使用–rewrite-db=‘from_name-> to_name’选项。这会将所有from_name重写为to_name。

shell>mysqlbinlog /data/mysql/binlogs/server1.000001 --start-position=xxx --stop-position=yyyy --rewrite-db='employees->employees_dev'

在恢复时禁用二进制日志

在恢复二进制日志的过程中,如果你不希望mysqlbinlog进程创建二进制日志,则可以使用–disable-log-bin选项:

shell>mysqlbinlog /data/mysql/binlogs/server1.00001 --start-position=xxx --stop-position=yyyy --disable-log-bin > binlog_restore

可以看到SET @OLD_SQL_LOG_BIN=@@SQL_LOG_BIN, SQL_LOG_BIN=0 被写到binlog_restore里,这样可以防止创建binlog

忽略要写入二进制日志的数据库

可以通过在my.cnf中指定–binlog-do-db=db_name选项,来选择将哪些数据库写入二进制日志。要指定多个数据库,就必须使用此选项的多个实例。由于数据库的名字可以包含逗号,因此如果提供逗号分隔列表,则该列表将被视为单个数据库的名字。需要重新启动MySQL服务器才能使更改生效。

迁移二进制日志

由于二进制日志占用越来越多的空间,有时你可能希望更改二进制日志的位置,可以按照以下步骤操作。单独更改 log_bin 是不够的,必须迁移所有二进制日志并在索引文件中更新位置。mysqlbinlogmove工具可以自动执行这些任务,简化你的工作。

具体操作:

先安装MySQL工具集,以使用mysqlbinlogmove脚本

1.systemctl stop mysql
2.mysqlbinlogmove --bin-log-basename=server1 --binlog-dir=/data/mysql/binlogs /binlogs
3.vim /etc/my.cnf
[mysqld]
log_bin=/binlogs
4.systemctl start mysql

将二进制日志从/data/mysql/binlogs更改为/binlogs,则应使用上面命令。如果base name不是默认名称,则必须通过–bin-log-basename 选项设定base name

育儿篇–1岁小孩的认知

1岁多的小孩,如果把他11个月大时很喜欢的玩具递给他,他会感到很无聊而走开。如果让他玩难度太大的游戏,他也会抗拒。他会对机械设备特别感兴趣,比如发条玩具、开关、纽扣和把手(昨天淘宝上买了点发条玩具,希望安安能喜欢)。你很难判断他这个年龄到底能做到什么、不能做到什么,但他自己决定并不难。为他提供一系列的丰富活动,他会自己挑选出一个具有挑战性,而又不是彻底超出他能力范围的游戏。

在这个年龄,模仿也是学习过程的重要部分。1岁以下的孩子只是单纯摆弄各种家什,但现在他会真的用梳子去梳头发、对着电话咿咿呀呀、扭玩具车的方向盘推前推后。起初,他只会自己进行这些活动,但渐渐地他会加入其他玩伴,如给娃娃梳头(看来要给她买个娃娃),拿一本他的书“给你讲”,递给玩伴一杯不存在的饮料,或者把玩具电话贴在耳朵上。因为现在模仿在他的行为和学习中扮演着极为重要的角色,所以你需要注意自己的行为并为他树立榜样。记住,你说过的话或做过的事可能在他游戏和学习过程中被一次次重演(也许是你喜欢的事,也许是你讨厌的事)。家里的大孩子也很重要,这种复制行为也会出现在幼儿和他的哥哥姐姐之间。现在正是利用这些自然发育行为的理想时机。

即将满2岁时,幼儿很擅长玩藏东西的游戏,物体离开他们视线很久后依然记得。如果你把他正在玩的球或小饼干藏起来,即使你忘得一干二净,他也绝对不会忘。

随着他藏猫猫玩得越来越好,他也会更加理解和你分离的时刻。就像他知道被藏起来的东西仍然存在于某个地方,即使他看不到,他现在也意识到你肯定会回来,即使你离开他一整天。如果你将要去的地方展示给他看——比如去工作或去商店,他会在脑海中形成一个你在那里的景象。这样可以让分离对他来说更简单。

这个年龄的幼儿很像指挥官,他会让你知道他希望你在他的活动中扮演什么角色。有时他会给你一个玩具,要你帮他弄好;有时他又会把玩具从你那里拿走,试着自己动手;有时当他知道自己做了什么很不寻常的事,他会停下来等你表扬他。通过回应这些信号,你会为他提供支持和鼓励。

你还必须为他做判断,因为他现在仍然缺乏判断能力。没错,他现在明白某些东西如何运行,但是,他不明白一件事如何影响另一件事,因此他还无法完全掌握事件的后果。例如,即使他知道玩具车会向低的地方滑,但他还是想不到把它放在车来车往的马路中间会发生什么;即使他知道门会打开关上,但他还不知道必须要小心不能让手被夹到;即使他经过很惨痛的教训,也很难保证他真正学到了东西。很有可能他根本没有把疼痛和引发疼痛的一连串事件联系在一起,而且几乎可以肯定他下次不会记得这一串因果关系——直到他有了自己的常识。目前你应该保持警觉,以保证他的安全。

幼儿对他的社交圈、朋友和熟人会形成一个具体印象。他是这个世界的中心,你可能在最亲近的位置,他最关心的是跟他有关的人事都在什么位置。他知道其他人存在,他对他们很感兴趣,但他完全不知道这些人的想法和感觉。在他的心目中,每个人都和他的想法一样。
2岁以下孩子的认知能力特点

■即使覆盖两三层东西,也能找出下面藏着的东西。
■开始按照形状和颜色排列东西。(颜色认知的书可以拿出来了)
■开始玩角色扮演(假装)游戏。

你可以想象,他对世界的这种看法(有些专家称此为“自我中心”)往往让他很难在真正的社交意义上和其他孩子一起玩。他会在旁边一起玩,一起抢玩具,但很难跟人配合一起做游戏。他喜欢观察其他孩子,喜欢被其他稍大的孩子围着。他会模仿他们或者像对待娃娃一样对待他们,例如帮他们梳头发,但当其他孩子要对他做同样的事情时,他会很惊讶然后抗拒。他还会主动给别人玩具或吃的东西,但如果别人真的拿走,他又可能很难过。

“分享”对这么大的孩子是个毫无意义的词。每个幼儿都认为他才应该是被关注的焦点。遗憾的是,大部分孩子不仅完全以自我为中心,而且自信的程度也不低,会跟人争夺玩具和关注,结果时常暴发冲突,最后大哭(人类的本能吧,这个时候只有本我,没有超我,想起总跟康康抢玩具)。如何在孩子的“朋友”来玩时减少冲突呢?尽量为每个孩子提供足够的玩具,然后时刻准备着去做调解员。

如我们前面建议的,孩子可能开始对属于他的玩具特别有占有欲。哪怕另一个孩子只是碰一下,他就会冲过来把玩具拿走。试着告诉他别的孩子“只是看看”而且“接下来会还给你”,但也要强调“是的,这是你的玩具,他不是要拿走”。可以选一些特别重要的东西,这些东西其他人不可以碰。有时这会让幼儿觉得对自己的世界有一定控制,从而让他对其他东西不那么有占有欲。

因为这个年龄段的孩子对他人的感受几乎没有任何考虑,所以他们对周围的孩子有时比较粗鲁。即使只是探索或表现好感的时候,他们也可能戳对方的眼睛或很用力地拍打(他们对动物也是这样)。不开心的时候他们会打人,却完全意识不到自己在伤害其他孩子。因此,当孩子和其他幼儿一起玩耍时一定要时刻小心,他一旦开始有攻击倾向就立刻将他拉开并告诉他“不能打人”,然后引导孩子们用友好的方式做游戏。

2岁以下孩子的社交能力特点
■模仿其他人的行为,特别是成人和年长的孩子。(所以父母是孩子的榜样)
■自我的独立意识提高。
■更加喜欢跟其他孩子在一起。

好在孩子也会用较温和的方式显示自我意识。在1岁半时,他就可以说出自己的名字差不多同一时间,他可以认出镜子里的自己,开始对照顾自己非常有兴趣。接近2岁时,如果大人教,他可以自己刷牙洗手。他还会配合穿衣,特别是脱衣。有时你一天会发现很多次他正忙着脱鞋脱袜子,即使你们正在商店里或者公园里。

因为幼儿是模仿大师,他会从你处理彼此之间矛盾的方式中学到重要的社交技巧。给他示范用语言和倾听来解决冲突(“我知道你想下来自己走,但你必须拉着我的手,这样我才知道你很安全”)。作为模仿者,他会很积极地帮你做任何“家务事”。不管你是读报纸、扫地、剪草坪,还是做饭,他都想“帮忙”。尽管带着他要花更多时间,但试着把这变成一个游戏吧。如果你正在做的事不适合他帮忙,比如很危险或事情紧迫,那就另找一个“家务”给他做。不管用什么方法,不要打击他想帮你做事的积极性。帮助和分享都是重要的社交技能,他越快学会,你们大家就越快乐。