修改网卡名称

在 CentOS6 和 CentOS7 下,有个简单的修改网卡名称的方法, 就在 ifcfg- 文件里加上 HWADDR:

DEVICE=em2
HWADDR=52:54:00:74:c6:81
BOOTPROTO=static
IPADDR=10.19.28.250
NETMASK=255.255.255.0
ONBOOT=yes
TYPE=Ethernet

经测试,加上 HWADDR 之后网卡名称从 eth0 变成 em2 了,比修改 /etc/udev/rules.d/70-persistent-net.rules 要好,特别是在修改 vm image 网卡设置的时候,因为不用多启动一次 vm。

 

参考:

http://support.qacafe.com/knowledge-base/how-can-i-assign-persistent-names-to-network-interfaces/

https://access.redhat.com/discussions/1240213

从镜像中 exclude 文件或目录, 缩减镜像大小

我的目标是从在基于镜像创建虚拟机的时候 exclude 部分文件或目录,使得创建虚机更快。本来我想如果 qemu-image convert 的时候支持 exclude,那就简单了,但是不支持,查了更多资料以后,我发现 guestfish 的 tar-out 支持 exclude,但是出来的是 tar 包,而不是 image 了(virt-tar-out 这个命令和 tar-out 类似,但是不支持 exclude)。


下面举个栗子,记录一下过程。

我的 domain 是 vm1, 系统盘名称叫 vm1,数据盘(/home)叫 vm1_data。

首先,对于系统盘的 vm1,我打算不使用 tar-out 打包出来,而是还是用 qemu-image convert 命令转换成 qcow2 格式,即使 vm1 已经是 qcow2,转换之后的 image 大小也可能减小,而且我使用 -c 选项启用压缩,转换之后的 image 大概 900M,大小我能接受。

下面是完整的转换命令:

qemu-img convert -c -O qcow2 vm1 /mfs/vm1

这种方法有个问题,如果系统盘不止 vm1,还有其他盘,会有问题,但是这种情况几乎没有,暂不考虑。


对于数据盘 vm1_data,不用 image ,使用 tar-out,而且我准备 exclude 出名字为 log 或者 logs 的目录,所以我先要获取 log 或者 logs 的目录路径。

在 vm1 关机情况下,对整个 domain 查找 /home 下 log 或 logs 的目录:

guestfish -d vm1 -i sh \
"find /home -type d -name log -o -name logs "

结果是:

/home/op/assets_agent_v2/logs
/home/ossec/ossec/logs

然后,用 tar-out 打包出来(启用压缩),格式如下:

guestfish -d vm1 \
run : mount /dev/datavg/home / : \
tar-out / home.tgz compress:gzip \
"excludes:op/assets_agent_v2/logs/* ossec/ossec/logs/*"

home.tgz 即是数据盘中去除日志的目录,当然在系统的实现中,我们支持自定义 exclude 目录。


在开机状态下,不能使用 -d vm1,只能用 add vm1_data 这种格式,此次 find 不支持 -type d 类似的参数,只能使用简单的 find 目录:

guestfish add vm1_data \
: run : mount /dev/datavg/home / : find /

这条命令会显示所有的文件和目录,如果想去重 log 或者 logs 目录,可以做聚合。

然后 tar-out 命令类似下面:

guestfish add vm1_data \
: run : mount /dev/datavg/home / : \
tar-out / home.tgz compress:gzip \
"excludes:op/assets_agent_v2/logs/* ossec/ossec/logs/*"

由于在开机状态下,我们指定了 /home 的 image,如果 /home 有多个 image,那么这种情况就需要关机了,系统中应该判断,如果有多个数据盘,需提示关机。


那么,对于 tar-out 出来的包,怎么「塞进」新的数据盘呢。

先创建 vm2_data:

virsh vol-create-as –pool vm_storage_pool \
–name vm2_data –capacity 10G –allocation 1G \
–format qcow2

然后创建 lvm 和文件系统,和 vm1_data 保持一致:

virt-format -a vm2_data –lvm=/dev/datavg/home \
–filesystem=ext4

查看 lvm group:

# virt-filesystems -a vm2_data –volume-groups
/dev/datavg

查看 physical volumes:

# virt-filesystems -a vm2_data –physical-volumes
/dev/sda1

查看文件系统:

# virt-filesystems -a vm2_data -l

Name Type VFS Label Size Parent
/dev/datavg/home filesystem ext4 – 10733223936 –

准备工作完成了,可以 tar-in 数据了:

guestfish add vm2_data \
: run : mount /dev/datavg/home / : \
tar-in home.tgz / compress:gzip

如果 home.tgz 通过 http 获取,可以这样:

curl http://nosa.me/vm2_home.tgz | \
guestfish add vm2_data : run : \
mount /dev/datavg/home / : \
tar-in – / compress:gzip

最后,借贵地记录一下 allocation 和 capacity 的说明。

allocation
Providing the total storage allocation for the volume. This may be smaller than the logical capacity if the volume is sparsely allocated. It may also be larger than the logical capacity if the volume has substantial metadata overhead. This value is in bytes. If omitted when creating a volume, the volume will be fully allocated at time of creation. If set to a value smaller than the capacity, the pool has the option of deciding to sparsely allocate a volume. It does not have to honour requests for sparse allocation though. Different types of pools may treat sparse volumes differently. For example, the logical pool will not automatically expand volume’s allocation when it gets full; the user is responsible for doing that or configuring dmeventd to do so automatically.By default this is specified in bytes, but an optional attribute unit can be specified to adjust the passed value. Values can be: ‘B’ or ‘bytes’ for bytes, ‘KB’ (kilobytes, 103 or 1000 bytes), ‘K’ or ‘KiB’ (kibibytes, 210 or 1024 bytes), ‘MB’ (megabytes, 106 or 1,000,000 bytes), ‘M’ or ‘MiB’ (mebibytes, 220 or 1,048,576 bytes), ‘GB’ (gigabytes, 109 or 1,000,000,000 bytes), ‘G’ or ‘GiB’ (gibibytes, 230 or 1,073,741,824 bytes), ‘TB’ (terabytes, 1012 or 1,000,000,000,000 bytes), ‘T’ or ‘TiB’ (tebibytes, 240 or 1,099,511,627,776 bytes), ‘PB’ (petabytes, 1015 or 1,000,000,000,000,000 bytes), ‘P’ or ‘PiB’ (pebibytes, 250 or 1,125,899,906,842,624 bytes), ‘EB’ (exabytes, 1018 or 1,000,000,000,000,000,000 bytes), or ‘E’ or ‘EiB’ (exbibytes, 260 or 1,152,921,504,606,846,976 bytes). Since 0.4.1, multi-character unit since 0.9.11
capacity
Providing the logical capacity for the volume. This value is in bytes by default, but a unit attribute can be specified with the same semantics as for allocationThis is compulsory when creating a volume. Since 0.4.1

redis 连接一直是 ESTABLISHED 的问题排查

昨天想删一台机器,发现上面还有 redis 连接:

# netstat -nat |grep ESTABLISHED |grep 6379
tcp 0 0 10.0.27.92:6379 10.0.27.157:24044 ESTABLISHED
tcp 0 0 10.0.27.92:6379 10.0.96.27:28975 ESTABLISHED
tcp 0 0 10.0.27.92:6379 10.0.69.47:58511 ESTABLISHED
tcp 0 0 10.0.27.92:6379 10.16.29.9:44571 ESTABLISHED
tcp 0 0 10.0.27.92:6379 10.0.29.49:48137 ESTABLISHED
tcp 0 0 10.0.27.92:6379 10.0.69.46:8854 ESTABLISHED
tcp 0 0 10.0.27.92:6379 10.0.70.67:42271 ESTABLISHED
tcp 0 0 10.0.27.92:6379 10.0.70.67:42269 ESTABLISHED
tcp 0 0 10.0.27.92:6379 10.0.24.30:17776 ESTABLISHED
tcp 0 0 10.0.27.92:6379 10.0.22.91:17823 ESTABLISHED
tcp 0 0 10.0.27.92:6379 10.0.23.79:59200 ESTABLISHED
tcp 0 0 10.0.27.92:6379 10.0.24.30:46296 ESTABLISHED
tcp 0 0 10.0.27.92:6379 10.0.23.98:31277 ESTABLISHED
tcp 0 0 10.0.27.92:6379 10.0.22.118:40458 ESTABLISHED
tcp 0 0 10.0.27.92:6379 10.16.29.9:44548 ESTABLISHED

这些连接一直都在,更奇怪的是,10.0.70.67 这个 IP 已经 ping 不通了,连接却不会断,正常情况下 tcp keepalive 会隔段时间检查一次,发现不通之后会发送 reset 。

 

为了看看到底有没有发送 tcp keepalive ack 包,抓个包看看,命令是 tcpdump -i em2 port 6379,下面是抓了一夜的包:redis.cpap

用 wireshark 分析,发现基本都是 keepalive ack 的包,取 10.0.96.27 这个IP,截个图看看:

4888351E-2D1D-4E6F-8062-F2DA259CF9C7

可以看到,10.0.96.27 主动 发送 ACK(SEQ: M、ACK: N)给 redis,redis 回复 ACK(SEQ: N、ACK:M+1),且 Len 都是0。

这能够解释大部分 IP 一直在 ESTABLISHED,因为一直有 tcp keepalive,但是 10.0.70.67 解释不通了,而且上面根本没抓到 10.0.70.67 的包,这只有一种可能: redis 不主动发送 keepalive。

 

找了下文档,发现 redis 确实默认关闭 tcp keepalive,所以对于已经建立的连接,不会发送 tcp keepalive ack 来确认对方存活,而如果对方突然死机或者关电源导致对方不主动关闭连接,那么 redis 就一直认为对方是活的,就不会去关闭连接了。

redis 提供了配置文件来更改这一默认行为:

# TCP keepalive.
#
# If non-zero, use SO_KEEPALIVE to send TCP ACKs to clients in absence
# of communication. This is useful for two reasons:
#
# 1) Detect dead peers.
# 2) Take the connection alive from the point of view of network
# equipment in the middle.
#
# On Linux, the specified value (in seconds) is the period used to send ACKs.
# Note that to close the connection the double of the time is needed.
# On other kernels the period depends on the kernel configuration.
#
# A reasonable value for this option is 60 seconds.
tcp-keepalive 0

 

事实上,如果 redis 对 idle 有时间限制,我遇到的情况也不会存在,但是 redis 也确实默认对 idle 的连接不加时间限制, 只是提供了 timeout 参数来更改。

 

解决 HTTP 请求被 LVS reset 的问题

最近两天遇到了 HTTP 请求被 LVS reset 的问题,LVS 默认有三个超时参数,如下:

# ipvsadm -L –timeout
Timeout (tcp tcpfin udp): 90 3 300

第一个就是 TCP 的空闲连接时间,这里超过 90 s 连接没数据会被 LVS reset 掉,在 HTTP Client 端可以看到 Connection reset by peer。我的某个服务 HTTP 请求时间很长,至少有 5 min,所以每次都会被 reset。

 

一开始我想到的解决办法是在 HTTP Client (HTTP 请求使用 requests)指定 so_keepidle参数小于 90 s。

因为 requests 使用 urllib3,而 urllib3 使用标准库 http.client(Python 3),Python 2.x 版本使用 httplib(它们会调用 socket.create_connection),所以我们可以封装一下来实现修改 so_keepidle:

import httplib
orig_connect = httplib.HTTPConnection.connect
def monkey_connect(self):
    orig_connect(self)
    self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
    self.sock.setsockopt(socket.SOL_TCP, socket.TCP_KEEPIDLE, 10)
    # sock.setsockopt(socket.SOL_TCP, socket.TCP_KEEPINTVL, 10)
    # sock.setsockopt(socket.SOL_TCP, socket.TCP_KEEPCNT, 100)
httplib.HTTPConnection.connect = monkey_connect
import requests

但是抓包发现 HTTP Client 端根本不发送 TCP keepalive 的包来保活,很是奇怪(可能是这段代码有问题,引用的还是老的模块,没继续debug,mark)。

 

后来发现可以在 Nginx 来实现 so_keepidle(LVS 后面是 Nginx),而且 Nginx 支持 so_keepidle 的配置,在 linsten 后面指定,格式如下:

222111h4s4ipokimf3zppr

比如:listen 80 so_keepalive=60::;

实测有效,终于解决了因为 HTTP 请求时间太长被 LVS reset 的问题,而且对域名生效,不影响其他域名(如果修改 LVS TCP idle timeout,对所有域名都有影响)。

 

参考:

http://stackoverflow.com/questions/15148225/is-it-possible-to-override-the-default-socket-options-in-requests

https://linux.cn/article-3766-1.html

http://nginx.org/en/docs/http/ngx_http_core_module.html

 

TCP 数据包格式

做个备忘。

TCP协议间交换的数据单元称为“TCP段”,包括两部分:首部和数据。标准首部长20字节,带有选项的首部会更长,选项最多 40 字节。

C9FBBF9A-04FE-4F2B-81E8-559391153C73

1. 源端口和目的端口

各占2字节,端口号加上IP地址,共同构成socket。互相通信的进程使用一对socket,包括协议、源IP、源端口、目的IP、目的端口,这五个元素唯一确定一个TCP连接。

2. 序号

占4字节,是TCP段所发送的数据部分第一个字节的序号。在TCP传送的数据流中,每一个字节都有一个序号。建立连接时,发送方将初始序号(Initial Sequence Number, ISN)填写到第一个发送的TCP段序号中。

3. 确认号

占4字节,是期望收到对方下次发送的数据的第一个字节的序号,也就是期望收到的下一个TCP段的首部中的序号,等于已经成功收到的TCP段的最后一个字节序号加1。确认号在ACK标志为1时有意义,除了主动发起连接的第一个TCP段不设置ACK标志外,其后发送的TCP段都会设置ACK标志。

4. 数据偏移

占4比特,表示数据开始的地方离TCP段的起始处有多远。实际上就是TCP段首部的长度。由于首部长度不固定,因此数据偏移字段是必要的。数据偏移以32位为长度单位,因此TCP首部的最大长度是60(15*4)个字节。

5. 控制位

一共6个,占6比特,设置为1时有效。按顺序依次为:URG、ACK、PSH、RST、SYN、FIN。

AED89D8A-403B-42F4-AA2E-3EC23D20FAB7

占2字节,表示报文段发送方期望接收的字节数,可接收的序号范围是从接收方的确认号开始到确认号加上窗口大小之间的数据。

6. 窗口

占2字节,表示报文段发送方期望接收的字节数,可接收的序号范围是从接收方的确认号开始到确认号加上窗口大小之间的数据。

7. 校验和

校验和包含了伪首部、TCP首部和数据,校验和是TCP强制要求的,由发送方计算,接收方验证。

8. 紧急指针

URG标志为1时,紧急指针有效,表示数据需要优先处理。紧急指针指出在TCP段中的紧急数据的最后一个字节的序号,使接收方可以知道紧急数据共有多长。

9. 选项   

最常用的选项是最大段大小(Maximum Segment Size,MSS),向对方通知本机可以接收的最大TCP段长度。MSS选项只在建立连接的请求中发送。
 
 
 
下面再介绍下选项。
典型的TCP头部选项结构如图3-4所示。
 
2A9044E0-E4AE-4801-B548-2CE4529ECB41
 
选项的第一个字段 kind 说明选项的类型。有的TCP选项没有后面两个字段,仅 包含 1字节的kind 字段。第二个字段 length(如果有的话)指定 该选项的总长度,该长度 包括 kind字段 和 length 字段占据的 2字节。第三个字段info(如果有的话)是选项的具体信息。常见的TCP选项有7种,如图3-5所示。
 
A35DFED0-B06F-4A59-859C-6B9B28688105
 
 
kind=0是选项表结束选项。
 
kind=1 是空操作(nop)选项,没有特殊含义,一般用于将TCP选项的总长度填充为4字节的整数倍。
 
kind=2 是最大报文段长度选项。TCP连接初始化时,通信双方使用该选项来协商最大报文段长度(Max Segment Size,MSS)。TCP模块通常将MSS设置为(MTU-40)字节(减掉的这40字节包括20字节的TCP头部和20字节的IP头部)。这样携带TCP报文段的IP数据报的长度就不会超过MTU(假设TCP头部和IP头部都不包含选项字段,并且这也是一般情况),从而避免本机发生IP分片。对以太网而言,MSS值是1460(1500-40)
字节。
 
kind=3 是窗口扩大因子选项。TCP连接初始化时,通信双方使用该选项来协商接收通告窗口的扩大因子。在TCP的头部中,接收通告窗口大小 是用16位表示的,故最大为65535 字节,但实际上TCP模块允许的接收通告窗口大小远不止这个数(为了提高TCP通信的吞吐量)。窗口扩大因子 解决了这个问题。假设TCP头部中的接收通告窗口大小是N,窗口扩大因子(移位数)是M,那么TCP报文段的实际接收通告窗口大小是N乘2M,或者说N左移M位。注意,M的取值范围是0~14。我们可以通过修改 /proc/sys/net/ipv4/tcp_window_scaling 内核变量来启用或关闭窗口扩大因子选项。
 
和MSS选项一样,窗口扩大因子选项只能出现在同步报文段中,否则将被忽略。但同步报文段本身不执行窗口扩大操作,即同步报文段头部的接收通告窗口大小就是该TCP报文段的实际接收通告窗口大小。当连接建立好之后,每个数据传输方向的窗口扩大因子就固定不变了。关于窗口扩大因子选项的细节,可参考标准文档RFC 1323。
 
kind=4 是选择性确认(Selective Acknowledgment,SACK)选项。TCP通信时,如果某个TCP报文段丢失,则TCP模块会重传最后被确认的TCP 报文段后续的所有报文段,这样原先已经正确传输的TCP报文段也可能重复发送,从而降低了TCP性能。SACK技术正是为改善这种情况而产生 的,它使TCP模块只重新发送丢失的TCP报文段,不用发送所有未被确认的TCP报文段。选择性确认选项用在连接初始化时,表示是否支持SACK技术。我们可以通过修改/proc/sys/net/ipv4/tcp_sack内核变量来启用或关闭选择性确认选项。
kind=5 是SACK实际工作的选项。该选项的参数 告诉 发送方 本端 已经收到 并缓存的 不连续 的 数据块,从而让发送端可以据此检查并重发丢失的 数据块。每个块边沿(edge of block)参数包含一个4字节的序号。其中块左边沿表示不连续块的第一个数据的序号,而块右边沿则表示不 连续块的最后一个数据的序号的下一个序号。这样一对参数(块左边沿和块右边沿)之间的数据是没有收到的。因为一个块信息占用8字节,所以TCP头部选项中实际上最多可以包含4个这样的不连续数据块(考虑 选项类型 和 长度占用的2字节 )。
 
kind=8 是时间戳选项。该选项提供了较为准确的计算通信双方之间的回路时间(Round Trip Time,RTT)的方法,从而为TCP流量控制提供重要信息。我们可以通过修改/proc/sys/net/ipv4/tcp_timestamps内核变量来启用或关闭时间戳选项。