df -h 卡住不动问题解决

早上测试环境打不开,登录看测试环境负载较高,页面提示貌似是磁盘满了。于是df -h想查一下磁盘情况,结果发现 df -h 卡住不动

于是mount -l 看了一下该机器上的目录情况,发现有一个

ip:port on /data/xxx type fuse.mfs (rw,nosuid,nodev,relatime,user_id=0,group_id=0,allow_other)

telnet访问了一下该ip和port, 发现策略不通,机器已经被回收了,所以现在df -h 打不开,
于是用umount -l /data/xxx 解除挂载

最后再次df -h,发现ok了。

linux截取部分文件方法

今天值班,需要定位一个job 的报错的问题,查看log文件时,发现job log 没有rotate, 一个文件几个G, 但是我只想看最新的文件末尾100w行,于是便有了这篇文章:

  1. 如果你只想看文件的前100行,可以使用head命令,如
    head -100  filename
  2. 如果你想查看文件的后100行,可以使用tail命令,如:
    tail -100  filename 或 tail -n 100  filename
  3. 查看文件中间一段,你可以使用sed命令,如:
    sed -n ‘100,200p’ filename 
    这样你就可以只查看文件的第100行到第200行。

截取的文件可以用重定向输入到新的文件中:
head -100  filename > a.txt

Ping与ICMP协议

ping 程序是用来探测 主机到主机之间是否可通信,如果不能 ping到某台主机,表明不能和这台主机建立连接。 ping 使用的是 ICMP协议, 它发送icmp回送请求消息给目的主机。 ICMP协议规定:目的主机必须返回ICMP回送应答消息给源主机。如果源主机在一定时间内收到应答,则认为主机可达。    ICMP协议通过IP协议发送的,IP协议是一种无连接的,不可靠的数据包协议。在Unix/Linux,序列号从0开始计数,依次递增。而Windows ping程序的ICMP序列号是没有规律。    ICMP协议在实际传输中数据包:20字节IP首部 + 8字节ICMP首部+ 1472字节<数据大小>38字节ICMP报文格式:IP首部(20字节)+8位类型+8位代码+16位校验和+(不同的类型和代码,格式也有所不同)

Ping工作过程——
    假定主机A的IP地址是192.168.1.1,主机B的IP地址是192.168.1.2,都在同一子网内,则当你在主机A上运行“ Ping 192.168.1.2”后,都发生了些什么呢?
首先, Ping命令会构建一个固定格式的ICMP请求数据包,然后由ICMP协议将这个数据包连同地址“192.168.1.2”一起交给 IP层协议(和ICMP一样,实际上是一组后台运行的进程),IP层协议将以地址“192.168.1.2”作为目的地址,本机IP地址作为源地址,加上一些其他的控制信息,构建一个IP数据包,并在一个映射表中查找出IP地址192.168.1.2所对应的物理地址(也叫MAC地址,熟悉网卡配置的朋友不会陌生, 这是数据链路层协议构建数据链路层的传输单元——帧所必需的),一并交给数据链路层。后者构建一个数据帧,目的地址是IP层传过来的物理地址,源地址则是本机的物理地址,还要附加上一些控制信息,依据以太网的介质访问规则,将它们传送出去。    其中映射表由ARP实现。ARP(Address Resolution Protocol)是地址解析协议,是一种将IP地址转化成物理地址的协议。ARP具体说来就是将网络层(IP层,也就是相当于OSI的第三层)地址解析为数据连接层(MAC层,也就是相当于OSI的第二层)的MAC地址。

    主机B收到这个数据帧后,先检查它的目的地址,并和本机的物理地址对比,如符合,则接收;否则丢弃。接收后检查该数据帧,将IP数据包从帧中提取出来,交给本机的IP层协议。同样,IP层检查后,将有用的信息提取后交给ICMP协议,后者处理后,马上构建一个ICMP应答包,发送给主机A,其过程和主机A发送ICMP请求包到主机B一模一样。即先由IP地址,在网络层传输,然后再根据mac地址由数据链路层传送到目的主机

ICMP——

1.IMCP协议介绍

前面讲到了,IP协议并不是一个可靠的协议,它不保证数据被送达,那么,自然的,保证数据送达的工作应该由其他的模块来完成。其中一个重要的模块就是ICMP(网络控制报文)协议。

当传送IP数据包发生错误--比如主机不可达,路由不可达等等,ICMP协议将会把错误信息封包,然后传送回给主机。给主机一个处理错误的机会,这 也就是为什么说建立在IP层以上的协议是可能做到安全的原因。ICMP数据包由8bit的错误类型和8bit的代码和16bit的校验和组成。而前 16bit就组成了ICMP所要传递的信息。

尽管在大多数情况下,错误的包传送应该给出ICMP报文,但是在特殊情况下,是不产生ICMP错误报文的。如下

  1. ICMP差错报文不会产生ICMP差错报文(出IMCP查询报文)(防止IMCP的无限产生和传送)
  2. 目的地址是广播地址或多播地址的IP数据报。
  3. 作为链路层广播的数据报。
  4. 不是IP分片的第一片。
  5. 源地址不是单个主机的数据报。这就是说,源地址不能为零地址、环回地址、广播地 址或多播地址。

虽然里面的一些规定现在还不是很明白,但是所有的这一切规定,都是为了防止产生ICMP报文的无限传播而定义的。

ICMP协议大致分为两类,一种是查询报文,一种是差错报文。其中查询报文有以下几种用途:

  1. ping查询
  2. 子网掩码查询(用于无盘工作站在初始化自身的时候初始化子网掩码)
  3. 时间戳查询(可以用来同步时间)

从 Ping 的工作过程,我们可以知道,主机A收到了主机B的一个应答包,说明两台主机之间的去、回通路均正常。也就是说,无论从主机A到主机B,还是从主机B到主机A,都是正常的。那么,是什么原因引起只能单方向 Ping 通的呢?

一般有以下几种可能:

1, 开启了防火墙

几乎所有的个人防火墙软件,默认情况下是不允许其他机器Ping本机的。一般的做法是将来自外部的ICMP请求报文滤掉,但它却对本机出去的ICMP请求报文,以及来自外部的ICMP应答报文不加任何限制。这样,从本机Ping其他机器时,如果网络正常,就没有问题。但如果从其他机器Ping这台机器,即使网络一切正常,也会出现“超时无应答”的错误。

2, 错误设置IP地址

有的服务器上有多块网卡,但存在两块网卡以上,同时将多网卡设成同一网段,在 IP 层协议看来,这台主机就有多个不同的接口处于同一网段内。当从这台主机 Ping 其他的机器时,会存在这样的问题:

(1)主机不知道将数据包发到哪个网络接口,因为有多个网络接口都连接在同一网段;

(2)主机不知道用哪个地址作为数据包的源地址。因此,从这台主机去 Ping 其他机器,IP 层协议会无法处理,超时后,Ping 就会给出一个“超时无应答”的错误信息提示。但从其他主机 Ping 这台主机时,请求包从特定的网卡来, ICMP 只须简单地将目的、源地址互换,并更改一些标志即可,ICMP 应答包能顺利发出,其他主机也就能成功 Ping 通这台机器了。

3, 路由配置的问题

还有一种可能引起单向ping通是由于网络中路由配置的问题,当路由设备的访问规则等其它配置不正确时会导致网络单向ping通,这个时候就要排查路由设备的配置问题

linux hostname修改

最近新增两台job给线上使用,装完机器,发现登录后命令提示符hostname是个ip,不便于识别机器,想改个hostname, 突然发现改hostname 这个操作自己好像之前操作的少,查了下,特此记录一下:

  • vim /etc/hostname
  • vim /etc/hosts 中127.0.0.1 的指向,最好加一条, 127.0.0.1 new_hostname
  • sudo hostnamectl set-hostname <newhostname>

最后重新登录会话,生效

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请求来临的时候是无法占用此端口。

服务器监控–top/w查看负载

  • 查看系统负载

如果服务很慢,我们通常会先看一下服务器的负载,可以使用命令w或是top

[root@VM_16_5_centos ~]# w
 11:10:25 up 298 days, 18:05,  1 user,  load average: 0.00, 0.01, 0.05
USER     TTY      FROM             LOGIN@   IDLE   JCPU   PCPU WHAT
root     pts/0    119.123.74.155   10:50    1.00s  0.04s  0.00s w

我们可以看到命令结果中,load average,他的意思是系统平均负载,里面三个数字,分别代表1分钟,5分钟,15分钟内系统的平均负载。

那么问题来了,这个值多少算比较合适呢?

当cpu完全空闲时,平均负载为0;当cpu工作满负载的时候,平均负载是1.

所以很显然,大家都知道,load average这个值越低越好。

判断系统负载是否过重,这里有篇不错的文章:“Unstanding linux cpu load

大致意思是:可以把cpu想象成一座桥,系统负载为0时是大桥上一辆车都没有。

如果系统负载为0.5的时候,意思是桥上只有一半的车行驶

如果系统负载为1, 大桥上所有路段都有车,桥上已经满了。但此时大桥还是能顺畅通行的。

如果系统负载为1.x, 意思是是大桥上已经跑满车,还有x0%的的车再等待上桥。系统负载越大,过桥就必须等待的越久。

回过头再来看系统负载多少合适?

当系统负载持续大于0.7, 就必须要着手调查了,可能是代码出了问题。

当系统负载持续大于0.7,就必须要解决了,降低负载。

当负载持续越大,可能系统就会出现不可用。

以上是单cpu的情况,现在生产环境的服务器,通常都是多cpu多核。

比如说:2个cpu, 表明系统负载可以达到2.0. 大于2.0 就是超负载。n个cpu的服务器,可以接受系统负载的最大值为n

多核cpu,一个cpu内部包含多个cpu core. 这被称为多核cpu。多核cpu和多cpu类似,所以这时的满负载值要考虑几个cpu, 每个cpu几个核,然后把系统负载除以总的核心数,只要不超过1。就表明服务器负载正常。

计算电脑cpu core数:

grep -c “model name” /proc/cpuinfo。返回cpu总核数。

linux curl 用法详解

最近在看elasticsearch相关文档,恶补下curl相关的知识,文章参考阮一峰的: http://www.ruanyifeng.com/blog/2019/09/curl-reference.html ,但是下面的命令自己都操作过,受益匪浅

curl 是常用的命令行工具,用于命令行发请求,curl 意为client url工具,类似curl的图形化界面工具有postman.

  • 最简单的用法,不带任何参数:get请求
curl http://www.baidu.com

                     

  • 指定客户端用户代理头,user-agent。curl的默认用户代理字符串是:curl/7.29.0,原始请求头可以通过 curl -v http://www.baidu.com查看,如果要指定客户端用户代理,可以用-A参
查看请求请求详情(Make the operation more talkative)
curl -A "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.100 Safari/537.36" -v http://www.baidu.com

                              

  • -H参数,直接指定请求头,( I参数是只看http返回头,不看返回体)
curl -H "User-Agent:Opera/9.80" -Iv http://www.baidu.com

                        

  • -b参数用来向服务器发送Cookie
curl -b "name=clarkhu" -vI http://www.baidu.com
请求发送cookie
  • 如果发送两个cookie
curl -b "foo1=bar;foo2=bar2" http://www.baidu.com

             

  • -c参数将服务器设置的cookie写入一个文件,下列命令将服务器http回应所设置的cookie写入文本文件cookie.txt
curl -c cookie.txt http://www.baidu.com

  • -d参数用于发送POST请求的数据体
curl -d'login=clark&password=123'-X POST https://baidu.com/login

  • -e 设置http referer
curl -e 'https://google.com?q=example' https://www.example.com

                      

  • -G参数用来构造url查询字符串
curl -G -d 'q=a' -d 'count=30' http://www.baidu.com/search

上面命令会发出一个get请求,实际url为http://www.baidu.com/search?q=a&count=30。如果省略G, 会发出一个POST请求

  • I参数向服务器发出HEAD请求,会将服务器返回的HTTP头打印出来,I参数等同于–head
  • L参数会让HTTP请求跟随服务器的重定向。curl默认不跟随重定向。
  • –limit-rate 用来限制HTTP请求和回应的带宽,模拟慢网速的环境,下面命令将带宽限制在每秒200k字节
curl --limit-rate 200k http://www.baidu.com
  • -O 参数将服务器回应保存成文件,并将url的最后部分当作文件名。
curl -O http://www.baidu.com
  • -u参数,用来设置服务器认证的用户名和密码
curl -u 'bob:12345' http://www.baidu.com/login
  • -v参数转出通信的整个过程,用于调试
curl -v http://www.baidu.com
  • –trace 也用于调试,并且会输出原始的二进制数据
curl --trace - http://www.baidu.com
  • 如何构建一个curl请求呢,可以利用chrome
console-copy-copy as cURL(bash)