ss命令在网络问题定位中的应用

引言

Linux系统中,了解当前的网络连接状态对于故障排查、网络性能调优至关重要。ss(socket statistics)命令是Linux系统中一种功能强大的工具,用于检查和显示网络连接相关的信息。本文将介绍 ss 命令的基本用法、常见选项和详细的命令示例。

1 ss的功能

下面按照ss -h手册中的参数来了解ss的基本用法:

  • 端口号不解析为协议:ss -n
    ss 默认会将端口号解析为协议,加-n选项后,则跳过解析流程,直接输出端口号

输出示例:在这里插入图片描述

对应源码处理逻辑如下:

static const char *resolve_service(int port)
{
	......
	//ss -n => numeric = 1
	if (numeric)
		goto do_numeric;
	//解析端口号
	......
do_numeric:
	sprintf(buf, "%u", port);
	return buf;
}
  • 显示所有套接字(所有协议+所有状态)信息:ss -a

输出示例:
在这里插入图片描述

源码中通过将过滤器的协议设置为all来实现

int main(int argc, char *argv[]) 
{
	......
	int do_default = 1;
	
	......
	if (do_default) {
		//state有值保持原值,无值则~((1<<SS_LISTEN)|(1<<SS_CLOSE)|(1<<SS_TIME_WAIT)|(1<<SS_SYN_RECV))
		state_filter = state_filter ? state_filter : SS_CONN;
		//过滤器的协议设置为all
		filter_db_parse(&current_filter, "all");
	}
	//ss -a => state_filter = SS_ALL;过滤器的连接状态设置为all
	filter_states_set(&current_filter, state_filter);
	filter_merge_defaults(&current_filter);
	
	......
}

  • 显示监听状态(state为LISTEN)套接字信息:ss -l

输出示例:
在这里插入图片描述

  • 显示计时器信息:ss -o

输出示例:
在这里插入图片描述

源码处理逻辑如下:

//ss -o => show_options = 1
if (show_options) {
		struct tcpstat t = {};

		t.timer = r->idiag_timer;
		t.timeout = r->idiag_expires;
		t.retrans = r->idiag_retrans;
		if (s->type == IPPROTO_SCTP)
			sctp_timer_print(&t);
		else
			tcp_timer_print(&t);
	}
//0:没有启动计时器 1:重传计时器
//2:keepalive计时器 3:timewait定时器
//4:0窗口探测计时器
static void tcp_timer_print(struct tcpstat *s)
{
	static const char * const tmr_name[] = {
		"off",
		"on",
		"keepalive",
		"timewait",
		"persist",
		"unknown"
	};

	if (s->timer) {
		if (s->timer > 4)
			s->timer = 5;
		out(" timer:(%s,%s,%d)",
			     tmr_name[s->timer],
			     print_ms_timer(s->timeout),
			     s->retrans);
	}
}

ss -o显示的timer:()具体含义如下:

timer的5种类型以及启用场景和功能:

  1. off:表示没有启用计时器
  2. on:表示开启了重传计时器,在计时器结束前没收到期待收到的报文,会触发超时重传
  3. keepalived:tcp保活计时器,应用程序若想使用 TCP 保活机制需要通过 socket 接口设置 SO_KEEPALIVE 选项才能够生效,保活倒计时结束时会向对端发送保活探测报文,对端正常响应则重制保活计时器,若未响应(如对端机器宕机等场景),会持续探测若干次,都未得到响应则会关闭该连接
    相关内核参数:net.ipv4.tcp_keepalive_intvl(发送 TCP 保活探测包之间的时间间隔)、net.ipv4.tcp_keepalive_probes ( TCP 保活探测包的最大数量)、net.ipv4.tcp_keepalive_time(TCP 连接空闲多长时间后开始发送保活探测包)

图片引用xiaolincoding.com
4. timewait:timewait状态定时器,主动断开连接的一方会进入timewait状态。timewait状态的作用如下:

  • 防止历史连接中的数据,被后面相同四元组的连接错误的接收:server端某个回包被网络延迟,client关闭连接后,相同五元组新建了一个连接,如果此时被网络延迟的报文(seq恰好在接收窗口内)到达client端,就会产生数据错乱等问题
    引用xiaolincoding.com
  • 保证被动关闭连接的一方,能被正确的关闭:最后一次ack可能在网络中丢掉,server端会重传fin包,如果没有timewait状态,client已经close掉了,收到这个fin包会回rst报文来断开引用xiaolincoding.com
  1. persist:0窗口探测定时器,如果接收方处理数据的速度跟不上接收数据的速度,接收缓冲区就会被占满,从而导致接收窗口为 0,会给发送方发送0窗口通知。发送方则不再发送数据,同时启动0窗口探测定时器,当定时器结束时,向接受方发送0窗口探测报文(发送序号为snd_una - 1、长度为0的ACK包作为探测包)
  • 显示详细的套接字(sockets)信息:ss -e

输出示例:在这里插入图片描述

static void sock_details_print(struct sockstat *s)
{
	//用户id
	if (s->uid)
		out(" uid:%u", s->uid);
	//inode编号
	out(" ino:%u", s->ino);
	//sk地址
	out(" sk:%llx", s->sk);
	......
}
  • 套接字分配内存大小信息:ss -m

输出示例:
在这里插入图片描述
skmem:(r2304,rb6291456,t0,tb69120,f1792,w0,o0,bl0,d61)

  • r2304:表示接收队列的内存使用量为 2304 字节
  • rb6291456:收缓冲区的大小为 6291456 字节
  • t0:发送队列的内存使用量为 0 字节
  • tb69120:发送缓冲区的大小为 69120 字节
  • f1792:套接字的预分配缓存大小剩余 1792 字节
  • w0:套接字的写缓冲区的内存使用量为 0 字节
  • o0:套接字的其他内存使用量为 0 字节
  • bl0:套接字的 backlog 队列的内存使用量为 0 字节
  • d61:丢弃的数据包数量为 61

内核中获取位置:

void sk_get_meminfo(const struct sock *sk, u32 *mem)
{
	memset(mem, 0, sizeof(*mem) * SK_MEMINFO_VARS);
	//&sk->sk_rmem_alloc
	mem[SK_MEMINFO_RMEM_ALLOC] = sk_rmem_alloc_get(sk);
	mem[SK_MEMINFO_RCVBUF] = sk->sk_rcvbuf;
	//&sk->sk_wmem_alloc
	mem[SK_MEMINFO_WMEM_ALLOC] = sk_wmem_alloc_get(sk);
	mem[SK_MEMINFO_SNDBUF] = sk->sk_sndbuf;
	//预分配缓存的大小
	mem[SK_MEMINFO_FWD_ALLOC] = sk->sk_forward_alloc;
	mem[SK_MEMINFO_WMEM_QUEUED] = sk->sk_wmem_queued;
	mem[SK_MEMINFO_OPTMEM] = atomic_read(&sk->sk_omem_alloc);
	mem[SK_MEMINFO_BACKLOG] = sk->sk_backlog.len;
	mem[SK_MEMINFO_DROPS] = atomic_read(&sk->sk_drops);
}

如果通过抓包发现有内核丢包,可以观察上述输出示例中 d的值,表示内核调用tcp_drop丢包的次数,tcp_drop->sk_drops_add(sk, skb)->atomic_add(segs, &sk->sk_drops)->sk_drops增加,ss -m显示的d会增加
常见会调用tcp_drop的点:
1、接收缓冲区满

static int tcp_try_rmem_schedule(struct sock *sk, struct sk_buff *skb,
				 unsigned int size)
{
	//如果当前已分配内存大于套接字接收缓冲区或在接收缓冲区中调度分配内存给数据包失败
	if (atomic_read(&sk->sk_rmem_alloc) > sk->sk_rcvbuf ||
	    !sk_rmem_schedule(sk, skb, size)) {
		//修剪合并缓冲区中内粗,经过修剪合并后,还是不够分配,表示缓冲区分配过度,进入tcp_drop流程
		//这个函数中会调整窗口大小,禁止快重传
		if (tcp_prune_queue(sk) < 0)
			return -1;
		//上一步修剪成功后,会再进行调度分配内存,调度失败则继续修剪
		while (!sk_rmem_schedule(sk, skb, size)) {
			if (!tcp_prune_ofo_queue(sk))
				return -1;
		}
	}
	return 0;
}

2、listen状态收到除syn以外的其他标志位的包

......
	case TCP_LISTEN:
		if (th->ack)
			return 1;

		if (th->rst)
			goto discard;

		if (th->syn) {
			if (th->fin)
				goto discard;
			/* It is possible that we process SYN packets from backlog,
			 * so we need to make sure to disable BH and RCU right there.
			 */
			rcu_read_lock();
			local_bh_disable();
			acceptable = icsk->icsk_af_ops->conn_request(sk, skb) >= 0;
			local_bh_enable();
			rcu_read_unlock();

			if (!acceptable)
				return 1;
			consume_skb(skb);
			return 0;
		}
		goto discard;
		......
discard:
		tcp_drop(sk, skb);
  • 显示进程相关信息:ss -p

输出示例:
在这里插入图片描述

  • 显示套接字线程相关信息:ss -T

输出示例: 在这里插入图片描述

  • 显示tcp详细信息:ss -i

输出示例:
在这里插入图片描述
bbr:当前使用的拥塞控制算法
wscale:7,2 :发送窗口缩放因子为7,接收窗口缩放因子为2
rto:201:超时重传时间201ms
rtt:0.928/0.008:往返时延为0.928ms,rtt的方差为0.013ms
mss:1260:tcp mss为1260字节
pmtu:1300 :链路mtu为1300字节(一般情况为1500字节,这里修改了网卡的mtu为1300用于测试)
rcvmss:536 : 接收方的 MSS为536
advmss:1260 :宣告的mss为1260字节
cwnd:110 :拥塞窗口大小为110
bytes_sent:10160621:已发送的字节数
bytes_acked:10160622:已确认的字节数
segs_out:21081:发送的段数
segs_in:21080:接收的段数
data_segs_out:21079:发送方成功发送的 TCP 数据段(segments)的数量
send 1.19Gbps :发送方的数据发送速率
lastsnd:2623:最后一次发送数据包的时间
lastrcv:266847492:最后一次接收数据包的时间
lastack:2622:最后一次收到ack报文的时间
pacing_rate 294Mbps :发送方控制数据发送速率的目标速率
delivery_rate 85.6Mbps:数据传输的速率,在特定时间段数据从发送方到接受方的速率
delivered:21080:已成功传输的包量
app_limited:应用层限制
busy:18613ms:连接处于活跃状态的时间
rcv_space:12600:接收缓冲区可用空间
rcv_ssthresh:65535:接收方慢启动阈值
minrtt:0.888 : 最小rtt
snd_wnd:41088:发送窗口大小

  • 显示套接字整体使用情况:ss -s

输出示例:在这里插入图片描述

走读ss源码可以了解到,ss -s的输出是通过调用get_sockstat函数,读取并解析/proc/net/sockstat中的内容

static int get_sockstat(struct ssummary *s)
{
	......
	//打开/proc/net/sockstat
	if ((fp = net_sockstat_open()) == NULL)
		return -1;
	//解析每一行的内容
	while (fgets(buf, sizeof(buf), fp) != NULL)
		get_sockstat_line(buf, s);
	......
}

/proc/net/sockstat中的内容通过内核sockstat_seq_show()函数来获取

static int sockstat_seq_show(struct seq_file *seq, void *v)
{
	struct net *net = seq->private;
	int orphans, sockets;
	
	orphans = percpu_counter_sum_positive(&tcp_orphan_count);
	sockets = proto_sockets_allocated_sum_positive(&tcp_prot);
	//获取socket总的使用数量,即sockets: used
	socket_seq_show(seq);
	seq_printf(seq, "TCP: inuse %d orphan %d tw %d alloc %d mem %ld\n",
		   sock_prot_inuse_get(net, &tcp_prot), orphans,
		   atomic_read(&net->ipv4.tcp_death_row.tw_count), sockets,
		   proto_memory_allocated(&tcp_prot));
	seq_printf(seq, "UDP: inuse %d mem %ld\n",
		   sock_prot_inuse_get(net, &udp_prot),
		   proto_memory_allocated(&udp_prot));
	seq_printf(seq, "UDPLITE: inuse %d\n",
		   sock_prot_inuse_get(net, &udplite_prot));
	seq_printf(seq, "RAW: inuse %d\n",
		   sock_prot_inuse_get(net, &raw_prot));
	seq_printf(seq,  "FRAG: inuse %u memory %lu\n",
		   atomic_read(&net->ipv4.frags.rhashtable.nelems),
		   frag_mem_limit(&net->ipv4.frags));
	return 0;
}
  • 显示特定协议套接字信息:ss -tMSudrx
    -t, --tcp display only TCP sockets
    -M, --mptcp display only MPTCP sockets
    -S, --sctp display only SCTP sockets
    -u, --udp display only UDP sockets
    -d, --dccp display only DCCP sockets
    -w, --raw display only RAW sockets
    -x, --unix display only Unix domain sockets
    –tipc display only TIPC sockets
    –vsock display only vsock sockets
    –xdp display only XDP sockets

  • ss筛选指定ip或端口:
    ss src xxx.xxx.xxx dst xxx.xxx.xxx
    ss sport OP xx dport OP xx

输出示例:
在这里插入图片描述
OP运算符:

  1. <= or le : 小于等于 >= or ge : 大于等于
  2. == or eq : 等于
  3. != or ne : 不等于端口
  4. < or lt : 小于这个端口
  5. > or gt : 大于端口

2 ss如何获取tcp套接字各种状态信息?

通过走读ss打印tcp相关信息源码,可以了解到:
ss会优先通过netlink套接字和内核通信,如果获取失败,则通过解析/proc/net/tcp来获取相关信息

static int tcp_show(struct filter *f)
{
	......

	if (getenv("TCPDIAG_FILE"))
		return tcp_show_netlink_file(f);
	//优先使用netlink与内核tcp_diag模块进行通信,效率高
	if (!getenv("PROC_NET_TCP") && !getenv("PROC_ROOT")
	    && inet_show_netlink(f, NULL, IPPROTO_TCP) == 0)
		return 0;
	//和tcp_diag通信失败才会去读/proc/net/tcp
	/* Sigh... We have to parse /proc/net/tcp... */
	......

	if (f->families & FAMILY_MASK(AF_INET)) {
		//打开/proc/net/tcp
		if ((fp = net_tcp_open()) == NULL)
			goto outerr;

		setbuffer(fp, buf, bufsize);
		//tcp_show_line解析/proc/net/tcp
		if (generic_record_read(fp, tcp_show_line, f, AF_INET))
			goto outerr;
		fclose(fp);
	}

	......
}

2.1 通过tcp_dig()获取

inet_show_netlink通过使用netlink套接字和内核进行通信,调用内核tcp_diag模块来获取到实时的tcp相关信息,调用链总结如下:inet_show_netlink->netlink层->sock_diag层->inet_diag层->tcp_diag
tcp_diag获取tcp相关信息的处理逻辑如下:

//获取 TCP 连接的详细信息
static void tcp_diag_get_info(struct sock *sk, struct inet_diag_msg *r,
			      void *_info)
{
	
	struct tcp_info *info = _info;
	//如果当前套接字处于listen状态
	//则rqueue代表当前accpet 队列的大小(也就是当前已完成三次握手并等待服务端 accept() 的 TCP 连接个数)
	//wqueue代表该listen套接字accept队列最大长度
	if (inet_sk_state_load(sk) == TCP_LISTEN) {
		r->idiag_rqueue = sk->sk_ack_backlog;
		r->idiag_wqueue = sk->sk_max_ack_backlog;
	} 
	//connection状态的套接字
	else if (sk->sk_type == SOCK_STREAM) {
		const struct tcp_sock *tp = tcp_sk(sk);
		//接收缓冲区中还没有被应用程序读取的字节数
		r->idiag_rqueue = max_t(int, READ_ONCE(tp->rcv_nxt) -
					     READ_ONCE(tp->copied_seq), 0);
		//发送缓冲区中还没有被远端主机确认的字节数
		r->idiag_wqueue = READ_ONCE(tp->write_seq) - tp->snd_una;
	}
	//获取tcp套接字的详细信息
	if (info)
		tcp_get_info(sk, info);
}

2.2 通过/proc/net/tcp来获取

cat /proc/net/tcp可以观察到当前tcp套接字相关信息,每一列的含义可以在内核的proc_net_tcp.txt文档中了解到
在这里插入图片描述
在这里插入图片描述

ss源码通过tcp_show_line函数对/proc/net/tcp中的每一列做了解析,核心代码如下:

static int tcp_show_line(char *line, const struct filter *f, int family)
{
	......
	//将/proc/net/tcp中的st列的值转化为10进制,对应不同的链接状态
	int state = (data[1] >= 'A') ? (data[1] - 'A' + 10) : (data[1] - '0');

	if (!(f->states & (1 << state)))
		return 0;
	//解析源目ip
	proc_parse_inet_addr(loc, rem, family, &s.ss);
	......
	//解析每一列的值,保存到相关结构体中
	n = sscanf(data, "%x %x:%x %x:%x %x %d %d %u %d %llx %d %d %d %u %d %[^\n]\n",
		   &s.ss.state, &s.ss.wq, &s.ss.rq,
		   &s.timer, &s.timeout, &s.retrans, &s.ss.uid, &s.probes,
		   &s.ss.ino, &s.ss.refcnt, &s.ss.sk, &rto, &ato, &s.qack, &s.cwnd,
		   &s.ssthresh, opt);
	......
}

内核通过tcp4_proc_init_net来创建/proc/net/tcp条目,用来记录tcp连接的相关信息

static int __net_init tcp4_proc_init_net(struct net *net)
{
	if (!proc_create_net_data("tcp", 0444, net->proc_net, &tcp4_seq_ops,
			sizeof(struct tcp_iter_state), &tcp4_seq_afinfo))
		return -ENOMEM;
	return 0;
}

随后会通过tcp4_seq_show来获取套接字信息,存入/proc/net/tcp中

static int tcp4_seq_show(struct seq_file *seq, void *v)
{
	struct tcp_iter_state *st;
	struct sock *sk = v;

	seq_setwidth(seq, TMPSZ - 1);
	if (v == SEQ_START_TOKEN) {
		seq_puts(seq, "  sl  local_address rem_address   st tx_queue "
			   "rx_queue tr tm->when retrnsmt   uid  timeout "
			   "inode");
		goto out;
	}
	st = seq->private;

	if (sk->sk_state == TCP_TIME_WAIT)
		get_timewait4_sock(v, seq, st->num);
	else if (sk->sk_state == TCP_NEW_SYN_RECV)
		get_openreq4(v, seq, st->num);
	else
		get_tcp4_sock(v, seq, st->num);
out:
	seq_pad(seq, '\n');
	return 0;
}
static void get_tcp4_sock(struct sock *sk, struct seq_file *f, int i)
{
	......
	//重传定机器
	if (icsk->icsk_pending == ICSK_TIME_RETRANS ||
	    icsk->icsk_pending == ICSK_TIME_REO_TIMEOUT ||
	    icsk->icsk_pending == ICSK_TIME_LOSS_PROBE) {
		timer_active	= 1;
		timer_expires	= icsk->icsk_timeout;
	//0窗口探测定时器
	} else if (icsk->icsk_pending == ICSK_TIME_PROBE0) {
		timer_active	= 4;
		timer_expires	= icsk->icsk_timeout;
	//keepalive定时器
	} else if (timer_pending(&sk->sk_timer)) {
		timer_active	= 2;
		timer_expires	= sk->sk_timer.expires;
	//没开启定时器
	} else {
		timer_active	= 0;
		timer_expires = jiffies;
	}

	state = inet_sk_state_load(sk);
	if (state == TCP_LISTEN)
		rx_queue = sk->sk_ack_backlog;
	else
		/* Because we don't lock the socket,
		 * we might find a transient negative value.
		 */
		rx_queue = max_t(int, READ_ONCE(tp->rcv_nxt) -
				      READ_ONCE(tp->copied_seq), 0);

	seq_printf(f, "%4d: %08X:%04X %08X:%04X %02X %08X:%08X %02X:%08lX "
			"%08X %5u %8d %lu %d %pK %lu %lu %u %u %d",......);
}

3 使用ss定位问题相关案例

3.1 问题描述

client请求server整体响应慢,需要400+ms。server端的处理逻辑是将请求转发到lo的另一个端口。通过抓包看返回慢的原因是不断有超时重传。
在这里插入图片描述
诉求是:定位子机内走lo口tcp超时重传的原因

3.2 分析过程

通过ss -m打印的输出可以关注到,skmem中末尾打印的d2表示这条五元组对应的流在交互过程中被内核drop了2包.
根据上面我们对ss -m输出结果的分析可以了解到,这条流交互的过程中调用过tcp_drop()丢了2包,client发送这两包被丢弃了,迟迟得不到响应,产生超时重传,整体请求耗时增加
至此使用ss工具就可以得到一个初步结论:本次请求耗时长的原因是某些报文在内核被tcp_drop()丢弃,发送方产生超时重传(超时重传计时器和重传过的次数可以通过ss -o显示的timer字段来查,本例rto为200ms),导致整体耗时增加
在这里插入图片描述
在了解到是被tcp_drop()丢包后,就可以使用bpftrace等工具,对内核丢包调用tcp_drop()的栈进行更详细的打点定位,进一步揪其根因,本文主要讲述ss使用相关,后续trace定位过程这里便不再赘述

4 结论与反思

在学习到ss的各种用法后, 可以通过ss命令轻松的列出当前的网络连接各种信息,对于网络问题的定位有很大的价值。
比如上面介绍的这类可以稳定复现的case中,可以不用抓包,通过ss观察打印的相关参数定位到问题所在,能更快发现端倪节省很多时间成本。

参考资料:
ss源码:https://github.com/iproute2/iproute2/blob/main/misc/ss.c
linux-4.19.288内核源码:https://mirrors.edge.kernel.org/pub/linux/kernel/v4.x/linux-4.19.288.tar.gz
https://cloud.tencent.com/developer/article/1860819
https://cloud.tencent.com/developer/article/1599217
https://cloud.tencent.com/developer/article/1721800
https://www.cnblogs.com/peida/archive/2013/03/11/2953420.html
https://blog.spoock.com/2019/07/06/ss-learn/
https://mp.weixin.qq.com/s/gJZluv-JUqpajylyOSTn8g

  • 31
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值