Linux网络-数据包的发送过程

在Linux系统中,数据包是如何一步一步从应用程序到网卡并最终发送出去的,拿以太网的物理网卡一个UDP包的发送过程作为示例。

socket层

  • socket(…): 创建一个socket结构体,并初始化相应的操作函数,由于我们定义的是UDP的socket,所以里面存放的都是跟UDP相关的函数。

  • sendto(sock, …): 应用层的程序(Application)调用该函数开始发送数据包,该函数数会调用后面的inet_sendmsg。

  • inet_sendmsg: 该函数主要是检查当前socket有没有绑定源端口,如果没有的话,调用inet_autobind分配一个,然后调用UDP层的函数。

  • inet_autobind: 该函数会调用socket上绑定的get_port函数获取一个可用的端口,由于该socket是UDP的socket,所以get_port函数会调到UDP代码里面的相应函数。

UDP层

  • udp_sendmsg: udp模块发送数据包的入口,该函数较长,在该函数中会先调用ip_route_output_flow获取路由信息(主要包括源IP和网卡),然后调用ip_make_skb构造skb结构体,最后将网卡的信息和该skb关联。

  • ip_route_output_flow: 该函数会根据路由表和目的IP,找到这个数据包应该从哪个设备发送出去,如果该socket没有绑定源IP,该函数还会根据路由表找到一个最合适的源IP给它。 如果该socket已经绑定了源IP,但根据路由表,从这个源IP对应的网卡没法到达目的地址,则该包会被丢弃,于是数据发送失败,sendto函数将返回错误。该函数最后会将找到的设备和源IP塞进flowi4结构体并返回给udp_sendmsg。

  • ip_make_skb: 该函数的功能是构造skb包,构造好的skb包里面已经分配了IP包头,并且初始化了部分信息(IP包头的源IP就在这里被设置进去),同时该函数会调用__ip_append_dat,如果需要分片的话,会在__ip_append_data函数中进行分片,同时还会在该函数中检查socket的send buffer是否已经用光,如果被用光的话,返回ENOBUFS。

  • udp_send_skb(skb, fl4) 主要是往skb里面填充UDP的包头,同时处理checksum,然后调用IP层的相应函数。

IP层

  • ip_send_skb: IP模块发送数据包的入口,该函数只是简单的调用一下后面的函数。

  • __ip_local_out_sk: 设置IP报文头的长度和checksum,然后调用下面netfilter的钩子。

  • NF_INET_LOCAL_OUT: netfilter的钩子,可以通过iptables来配置怎么处理该数据包,如果该数据包没被丢弃,则继续往下走。

  • dst_output_sk: 该函数根据skb里面的信息,调用相应的output函数,在我们UDP IPv4这种情况下,会调用ip_output。

  • ip_output: 将上面udp_sendmsg得到的网卡信息写入skb,然后调用NF_INET_POST_ROUTING的钩子。

  • NF_INET_POST_ROUTING: 在这里,用户有可能配置了SNAT,从而导致该skb的路由信息发生变化。

  • ip_finish_output: 这里会判断经过了上一步后,路由信息是否发生变化,如果发生变化的话,需要重新调用dst_output_sk(重新调用这个函数时,可能就不会再走到ip_output,而是走到被netfilter指定的output函数里,这里有可能是xfrm4_transport_output),否则往下走。

  • ip_finish_output2: 根据目的IP到路由表里面找到下一跳(nexthop)的地址,然后调用__ipv4_neigh_lookup_noref去arp表里面找下一跳的neigh信息,没找到的话会调用__neigh_create构造一个空的neigh结构体。

  • dst_neigh_output: 在该函数中,如果上一步ip_finish_output2没得到neigh信息,那么将会走到函数neigh_resolve_output中,否则直接调用neigh_hh_output,在该函数中,会将neigh信息里面的mac地址填到skb中,然后调用dev_queue_xmit发送数据包。

  • neigh_resolve_output: 该函数里面会发送arp请求,得到下一跳的mac地址,然后将mac地址填到skb中并调用dev_queue_xmit。

netdevice子系统

  • dev_queue_xmit: netdevice子系统的入口函数,在该函数中,会先获取设备对应的qdisc,如果没有的话(如loopback或者IP tunnels),就直接调用dev_hard_start_xmit,否则数据包将经过Traffic Control模块进行处理。

  • Traffic Control: 这里主要是进行一些过滤和优先级处理,在这里,如果队列满了的话,数据包会被丢掉,详情请参考文档,这步完成后也会走到dev_hard_start_xmit。

  • dev_hard_start_xmit: 该函数中,首先是拷贝一份skb给“packet taps”,tcpdump就是从这里得到数据的,然后调用ndo_start_xmit。如果dev_hard_start_xmit返回错误的话(大部分情况可能是NETDEV_TX_BUSY),调用它的函数会把skb放到一个地方,然后抛出软中断NET_TX_SOFTIRQ,交给软中断处理程序net_tx_action稍后重试(如果是loopback或者IP tunnels的话,失败后不会有重试的逻辑)。

  • ndo_start_xmit: 这是一个函数指针,会指向具体驱动发送数据的函数。

Device Driver

ndo_start_xmit会绑定到具体网卡驱动的相应函数,到这步之后,就归网卡驱动管了,不同的网卡驱动有不同的处理方式,这里不做详细介绍,其大概流程如下:

  • 将skb放入网卡自己的发送队列
  • 通知网卡发送数据包
  • 网卡发送完成后发送中断给CPU
  • 收到中断后进行skb的清理工作

在网卡驱动发送数据包过程中,会有一些地方需要和netdevice子系统打交道,比如网卡的队列满了,需要告诉上层不要再发了,等队列有空闲的时候,再通知上层接着发数据。

阅读全文

kubernetes技术介绍

Kubernetes技术介绍

白话容器(Docker)

容器VS虚拟机

虚拟机

  • 硬件虚拟化
  • 操作系统
  • 应用

容器

  • 共享操作系统
  • 应用

容器本质: 一组资源被限制和隔离的进程

限制与隔离

Linux限制技术

  • Linux Cgroup

Linux Cgroups 的全称是 Linux Control Group。它最主要的作用,就是限制一个进程组能够使用的资源上限,包括 CPU、内存、磁盘、网络带宽等等。

Linux隔离技术

  • PID Namespace
  • Mount Namespace
  • Net Namespace
  • IPC Namespace
  • User Namespace
  • UTS Namespace

Namespace 技术实际上修改了应用进程看待整个计算机“视图”,即它的“视线”被操作系统做了限制,只能“看到”某些指定的内容。

容器 Tech Demo

#define _GNU_SOURCE
#include <fcntl.h>
#include <sched.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>

int main(int argc, char *argv[])
{
    int pid_ns_fd;
    int net_ns_fd;
    int mnt_ns_fd;

    pid_ns_fd = open(argv[1], O_RDONLY);
    net_ns_fd = open(argv[2], O_RDONLY);
    mnt_ns_fd = open(argv[3], O_RDONLY);
    //printf("%s %s %s", argv[1], argv[2], argv[3]);

    // 设置pid namespace
    if (setns(pid_ns_fd, CLONE_NEWPID) < 0)
    {
        printf("set pid namespace error");
        return -1;
    }
    // 设置network namespace
    if (setns(net_ns_fd, CLONE_NEWNET) < 0)
    {
        printf("set ns namespace error");
        return -1;
    }
    // 设置mount namespace
    if (setns(mnt_ns_fd, CLONE_NEWNS) < 0)
    {
        printf("set ns namespace error");
        return -1;
    }
    // 执行命令
    pid_t pid = fork();
    if (pid < 0)
    {
        printf("fork error");
        return -1;
    }
    else if (pid == 0)
    {
        return execvp(argv[4], &argv[4]);
    }
    else
    {
        return waitpid(pid, NULL, 0);
    }
}
sudo ./setns /proc/11773/ns/pid /proc/11773/ns/net /proc/11773/ns/mnt bash

容器网络 overview

容器网络 veth pair

veth-pair 就是一对的虚拟设备接口,它都是成对出现的。一端连着协议栈,一端彼此相连着。

容器网络实践

  • 单机容器网络连通

容器到容器:通过docker0,直接互通
容器到宿主机/宿主机到容器:本地路由

  • 跨节点容器网络连通

一个简单不自动化方案:设置节点容器网段,在节点上配置路由

白话Kubernetes

  • Kubernetes架构
  • Kubernetes资源概念和使用
  • Kubernetes扩展

Kubernetes架构

Kubernetes资源概念和使用

资源类型

  • Nodes
  • Pods
  • Namespaces
  • Depolyments
  • Daemon Sets
  • Jobs
  • ConfigMaps
  • Secrets
  • ServiceAccounts
  • Services
  • more…

管理方式

kubectl (create|get|apply|delete) -f myResource.yaml

Kubernetes扩展

  • CRD
  • Operator模式

Kubernetes实践

Kubernetes部署

资源准备

  • Etcd
  • 机器

节点要求

  • 机器配置
  • 容器运行时
  • 内核参数
  • 网络设置

复杂点

  • 组件证书!!!
  • 组件参数

高可用

  • Etcd高可用
  • Master节点(Control plane)高可用

Kubernetes部署实践

Etcd

  • 动态平台etcd服务

master节点

  • kube-api-server
  • kube-controller-manager
  • kube-scheduler

node节点

  • kubelet
  • kube-proxy

Ansible demo

Kubernetes网络

Kubernetes网络约定

约定

  • Every pod gets its own IP address.
  • Containers within a pod share the pod IP address and can communicate freely with each other.
  • Pods can communicate with all other pods in the cluster using pod IP addresses (without NAT).
  • Isolation (restricting what each pod can communicate with) is defined using network policies.

网络实现和CNI(Container Network Interface)

Kubernetes组件中并没有实现约定的网络,而是制定了网络接口规范,将具体实现留给了插件。
简单来说,kubernetes组件中kubelet实现了cni插件接口,在创建Pod过程中调用插件,为Pod配置网络。

Kubernetes网络之overlay网络

Kubernetes网络约定思考

Kernernetes网络约定中"Pod到Pod的网络使用Pod IP无需Nat,即使跨节点",这定义了Cluster Pod网络是一个扁平的网络。
这样扁平的网络指什么呢?

Overlay

图中所示以vxlan实现举例基于四层(UDP),实现一个虚拟的二层网络。

Kubernetes网络插件之calico

回顾上文容器网络章节中,我们设计了一个方案“规划容器网段,配置路由”可实现容器跨节点网络,值得注意的是,这个简单方式所实现的网络是满足kubernetes的一个网络,容器之间通过IP互通,无需NAT。同时注意,这个网络No Overlay!。

calico

Calico主要由Felix、etcd、BGP client、BGP Route Reflector组成。

  • Etcd:负责存储网络信息
  • BGP client:负责将Felix配置的路由信息分发到其他节点
  • Felix:Calico Agent,每个节点都需要运行,主要负责配置路由、配置ACLs、报告状态
  • BGP Route Reflector:大规模部署时需要用到,作为BGP client的中心连接点,可以避免每个节点互联
阅读全文

eBPF开发

eBPF开发

libbpf-bootstrap

git clone git@github.com:libbpf/libbpf-bootstrap.git
git submodule update --init --recursive

开发环境

# ubuntu 20.04
sudo apt install clang clang-12
sudo apt install linux-image-5.8.0-63-generic linux-headers-5.8.0-63-generic linux-modules-extra-5.8.0-63-generic
sudo apt install libelf-dev pkg-config
sudo update-alternatives --install /usr/bin/clang clang /usr/bin/clang-10 1 --slave /usr/bin/clang++ clang++ /usr/bin/clang++-10
sudo update-alternatives --install /usr/bin/clang clang /usr/bin/clang-12 2 --slave /usr/bin/clang++ clang++ /usr/bin/clang++-12
sudo update-alternatives --install /usr/bin/llvm-strip llvm-strip /usr/bin/llvm-strip-10 1
sudo update-alternatives --install /usr/bin/llvm-strip llvm-strip /usr/bin/llvm-strip-12 2

相关链接

阅读全文

什么是eBPF

什么是eBPF

从Linux Tracing说起

在我们做程序调试和性能分析时候经常需要跟踪(Tracing)程序执行过程,收集执行信息,综合收集到的信息分析程序问题。基于这样的需求,Linux生态便出现了各种各样的工具,我们在编程开发中或多或少已经接触到了这样的工具,比如GDB、perf,甚至自己代码中的Printf也算工具之一。概括来讲,Tracing工具是实现“采集目标程序执行信息目的”的程序。

那么实现Tracing,需要解决什么问题呢?

  1. 数据源在哪里。
  2. 采集方式。

以GDB为例,我们知道GDB调试,需要开发人员编译加特定参数,编译器会在编译结果程序中加入特定Debug信息。这个加参数暴露Debug信息是一个下探针的过程,探针是Tracing数据的来源。然后我执行GDB命令,单步执行、输出状态信息,这是采集和使用Tracing数据的过程。

先加探针,然后采集调试的过程,在线上程序出性能问题的时候,很难实现,而且很多程序也是到线上场景才暴露性能问题的,能不能随时加探针,随时跟踪目标程序呢?操作系统作为应用程序的执行环境,可以拥有上帝视角,借助内核加探针使我们拥有随时加探针的超能力!

Linux Tracing技术不断发展,诞生了各种各样的工具,形成了一个Tracing系统生态。

在Linux内核中已经有了各种各样的探针,按照形态分为:

  • 硬件探针,硬件直接暴露信息,比如CPU cycles数。
  • 静态探针,重新编译内核或者通过加载内核模块形式下的探针,代表技术: Tracepoint。
  • 动态探针,可以动态在内核函数上添加Hook的探针机制,代表技术Kprobe。

简单深入下静态探针和动态探针,假设我们需要Hook一个内核函数,静态探针需要我们改内核代码,在目标函数前后加入我们采集数据的代码,动态探针则是我们动态指定对应函数和Hook脚本就可以了。相比静态探针需要重新编译内核添加新探针,动态探针灵活几乎可以Hook所有内核函数。

基于内核各种各样的探针,诞生了各种工具、库、框架(Kprobe,Tracepoint,LTTng,SystemTap,eBPF等),这些工具、库底层共用内核的探针技术,用户实现一个Tracing目的,可以选择使用不同的工具、库。

为什么是eBPF

eBPF并不是一个新的Tracing探针技术,而是新的Tracing框架技术,以更安全、更高效、更灵活的方式,方便用户实现Linux Tracing!

采用eBPF可以对接几乎已有的所有探针技术,eBPF的灵活形态,方便用户实现特定化需求。

eBPF工作原理

eBPF程序分为两部分:

  • 内核态代码,被加载进内核执行的代码,加载之前需要被验证器校验,确保不会对内核造成伤害。
  • 用户态代码,和我们正常实现一个应用代码没有区别,在用户进程空间执行,但是额外做一些帮助内核态代码load进内核的工作。

内核态代码会被限制可以调用的函数,通常干采集数据的工作,用户态代码调用函数不限,方便实现业务逻辑,那么内核态代码和用户态代码如何沟通数据呢?eBPF提供了bpf_maps,是用户态和内核态共享的数据空间,通过k-v结构共享存取数据。

内核态代码也不同与我们写正常C语言代码,编译成机器码二进制进入内核执行的,而是内核约定的一套Bytecode指令,eBPF的验证器验证ByteCode通过后,eBPF的JIT编译器将ByteCode编译成机器码进入内核执行。这样一套过程,验证确保安全,JIT确保高效。

我们写eBPF内核态代码不能直接写ByteCode(类似汇编)吧?目前Clang实现了将C语言代码编译成eBPF Bytecode的后端,所以可以使用C编写eBPF内核态代码。用户态代码是在eBPF程序用户进程执行,所以可以便随意选择编程语言。

例子:

内核态代码
#include <uapi/linux/ptrace.h>
#include <linux/sched.h>


struct key_t {
u32 prev_pid;
u32 curr_pid;
};

// 创建与用户态代码共享数据的eBPF maps
BPF_HASH(stats, struct key_t, u64, 1024);
int count_sched(struct pt_regs *ctx, struct task_struct *prev) {
struct key_t key = {};
u64 zero = 0, *val;


key.curr_pid = bpf_get_current_pid_tgid();
key.prev_pid = prev->pid;


// could also use `stats.increment(key);`
val = stats.lookup_or_try_init(&key, &zero);
if (val) {
(*val)++;
}
return 0;
}
用户态代码

#!/usr/bin/python
# Copyright (c) PLUMgrid, Inc.
# Licensed under the Apache License, Version 2.0 (the "License")

from bcc import BPF
from time import sleep

# 帮助load 内核态代码
b = BPF(src_file="task_switch.c")
# 指定内核态代码Hook点,指定探针
b.attach_kprobe(event_re="^finish_task_switch$|^finish_task_switch\.isra\.\d$",fn_name="count_sched")

# generate many schedule events
for i in range(0, 100): sleep(0.01)

for k, v in b["stats"].items():
print("task_switch[%5d->%5d]=%u" % (k.prev_pid, k.curr_pid, v.value))

相关链接

阅读全文

在离线业务混部

什么是在离线业务

  • 在线服务:运行时间长,服务流量及资源利用率有潮汐特征,时延敏感,对服务 SLA 要求极高,如消息流 Feed 服务、电商交易服务等。

  • 离线作业:运行时间分区间,运行期间资源利用率较高,时延不敏感,容错率高,中断一般允许重运行,如 Hadoop 生态下的 MapReduce、Spark 作业。

技术门槛

  • 可观测性体系
  • 调度决策

在离线混部的调度决策是决定混部效果的核心,目前主要有几种决策方式:

整机分时复用:在固定的时间点(比如凌晨以后)跑离线作业,白天让出资源给在线服务。这种以时间维度切分的混部方式比较简单易理解,但整体资源利用率提升有限。

资源部分共享:将单机的资源整体划分为在线资源、离线资源以及在离线共享资源,各资源之间隔离,提前划分预留。这种从单机资源维度切分的混部方式比分时复用相对更精细一些,但是需要资源规格较大的机器切分才有意义。

资源完全共享:通过及时准确的资源预测手段、快速响应资源变化的能力,以及一套可以在资源水位发生变化时的服务保障措施,更高效自动化地实现机器资源复用。资源归属不预设,完全依据实时指标决策。

前一种属于静态决策,相对来说对底层可观测性体系的要求、对调度系统的高可用高性能的要求较低。
后两种属于动态决策,在资源利用率的提升上比静态决策更优,但对前述支撑系统要求也更高。

  • 调度执行
  • 资源隔离
  • 任务冲突时的资源保障
  • 服务平行扩缩容能力

业界在离线混部方案

  • 独占内核 + 物理机 + 静态决策

入门级的在离线混部选择,比如物理机运行服务且分时整机腾挪。

  • 独占内核 + 容器 + 动态决策

如果公司研发团队底层技术积累比较少,想快速、安全、低成本地用上在离线混部,先享受部分混部的成本优化红利,则独占内核+ 容器 + 动态决策组合的方案是首选。

  • 共享内核 + 容器 + 动态决策

如果有比较强的研发实力,能够较好解决第二部分中讲到的几乎所有技术门槛,就可以挑战共享内核 + 容器 + 动态决策组合的方案,以追求极致的资源利用率和成本优化效果。

阅读全文