zoukankan      html  css  js  c++  java
  • vxlan 内核实现

    vxlan_tnl_send
    根据vxlan tunnel的ip查找路由。
    调用vxlan_xmit_skb封装发送报文。
    vxlan_xmit_skb
    计算封装vxlan需要的最小空间,并且扩展头部空间。
    添加vxlan头。
    如果有BGP的头,也添加。
    udp_tunnel_xmit_skb添加协议头发送。
    udp_tunnel_xmit_skb
    添加UDP协议头。
    iptunnel_xmit继续添加协议头,并且发送。
    iptunnel_xmit
    添加ip协议头。
    ip_local_out_sk–>__ip_local_out–>__ip_local_out_sk继续添加协议头,并且发送。
    __ip_local_out_sk
    过netfilter的LOCAL_OUT。
    调用dst_output_sk–>ip_output。
    ip_output
    过netfilter的POST_ROUTING。
    调用ip_finish_output
    ip_finish_output
    如果报文支持gso,调用ip_finish_output_gso进行分片。
    如果报文大于mtu,调用ip_fragment进行分片。
    调用ip_finish_output2进行报文发送。
    ip_finish_output2
    __ipv4_neigh_lookup_noref查找邻居子系统。
    调用dst_neigh_output–>neigh_hh_output进行报文发送。
    neigh_hh_output
    封装2层协议头。
    调用dev_queue_xmit进行报文发送

    Linux 内核支持 GSO for UDP tunnels

    • 需要在 skb 发到 UDP 协议栈之前,添加一个新的 option:inner_protocol,可以使用方法 skb_set_inner_ipproto 或者 skb_set_inner_protocol 来设置。vxlan driver 中的相关代码为 skb_set_inner_protocol(skb, htons(ETH_P_TEB));
    • 函数
      skb_udp_tunnel_segment 会检查该 option 再处理分段。
    • 支持多种类型的封装,包括 SKB_GSO_UDP_TUNNEL{_CSUM}

      其驱动设置了 net_device_ops结构体变量, 其中定义了操作 net_device 的重要函数,vxlan在驱动程序中根据需要的操作要填充这些函数,其中主要是 packets 的接收和发送处理函数。

    复制代码
    static const struct net_device_ops vxlan_netdev_ops = {
        .ndo_init        = vxlan_init,      
        .ndo_uninit        = vxlan_uninit,
        .ndo_open        = vxlan_open,
        .ndo_stop        = vxlan_stop,
        .ndo_start_xmit        = vxlan_xmit,  #向 vxlan interface 发送 packet
        ...
    };
    复制代码

    来看看代码实现:

    (1)首先看 static netdev_tx_t vxlan_xmit(struct sk_buff *skb, struct net_device *dev) 方法,它的输入就是要传输的 packets  所对应的 sk_buff 以及要经过的 vxlan interface dev:

    它的主要逻辑是获取 vxlan dev,然后为 sk_buff 中的每一个 skb 调用 vxlan_xmit_skb 方法。

    #该方法主要逻辑是,计算 tos,ttl,df,src_port,dst_port,md 以及 flags等,然后调用 vxlan_xmit_skb 方法。
    err = vxlan_xmit_skb(rt, sk, skb, fl4.saddr, dst->sin.sin_addr.s_addr, tos, ttl, df, src_port, dst_port, htonl(vni << 8), md, !net_eq(vxlan->net, dev_net(vxlan->dev)), flags);

    (2)vxlan_xmit_skb 函数修改了 skb,添加了 VxLAN Header,以及设置 GSO 参数。

    复制代码
    static int vxlan_xmit_skb(struct rtable *rt, struct sock *sk, struct sk_buff *skb,
                  __be32 src, __be32 dst, __u8 tos, __u8 ttl, __be16 df,
                  __be16 src_port, __be16 dst_port, __be32 vni,
                  struct vxlan_metadata *md, bool xnet, u32 vxflags)
    {
        ...int type = udp_sum ? SKB_GSO_UDP_TUNNEL_CSUM : SKB_GSO_UDP_TUNNEL; #计算 GSO UDP 相关的 offload type,使得能够利用内核 GSO for UDP Tunnel
        u16 hdrlen = sizeof(struct vxlanhdr); #计算 vxlan header 的长度
        ...
    #计算 skb 新的 headroom,其中包含了 VXLAN Header 的长度 min_headroom = LL_RESERVED_SPACE(rt->dst.dev) + rt->dst.header_len + VXLAN_HLEN + sizeof(struct iphdr) + (skb_vlan_tag_present(skb) ? VLAN_HLEN : 0); /* Need space for new headers (invalidates iph ptr) */ err = skb_cow_head(skb, min_headroom); #使得 skb head 可写 ... skb = vlan_hwaccel_push_inside(skb); #处理 vlan 相关事情 ... skb = iptunnel_handle_offloads(skb, udp_sum, type); #设置 checksum 和 type ... vxh = (struct vxlanhdr *) __skb_push(skb, sizeof(*vxh)); #扩展 skb data area,来容纳 vxlan header vxh->vx_flags = htonl(VXLAN_HF_VNI); vxh->vx_vni = vni; ... if (vxflags & VXLAN_F_GBP) vxlan_build_gbp_hdr(vxh, vxflags, md); skb_set_inner_protocol(skb, htons(ETH_P_TEB)); #设置 Ethernet protocol,这是 GSO 在 UDP tunnel 中必须要的 udp_tunnel_xmit_skb(rt, sk, skb, src, dst, tos, ttl, df, #调用 linux 网络栈接口,将 skb 传给 udp tunnel 协议栈继续处理 src_port, dst_port, xnet, !(vxflags & VXLAN_F_UDP_CSUM)); return 0; }
    复制代码

     

     (3)接下来就进入了 Linux TCP/IP 协议栈,从 UDP 进入,然后再到 IP 层。如果硬件支持,则由硬件调用 linux 内核中的 UDP GSO 函数;如果硬件不支持,则在进入 device driver queue 之前由 linux 内核调用 UDP GSO 分片函数。然后再一直往下到网卡。

    最终在这个函数 ip_finish_output_gso 里面,先调用 GSO分段函数,如果需要的话,再进行 IP 分片:


    static
    int ip_finish_output(struct sock *sk, struct sk_buff *skb) { #if defined(CONFIG_NETFILTER) && defined(CONFIG_XFRM) /* Policy lookup after SNAT yielded a new policy */ if (skb_dst(skb)->xfrm) { //仅经过ip_forward流程处理的报文携带该对象 IPCB(skb)->flags |= IPSKB_REROUTED; //该flag会影响后续报文的GSO处理 return dst_output_sk(sk, skb); //由于SNAT等策略处理,需要再次调用xfrm4_output函数来发包 } #endif if (skb_is_gso(skb)) return ip_finish_output_gso(sk, skb); //如果是gso报文 if (skb->len > ip_skb_dst_mtu(skb)) //非gso报文,报文大小超过设备MTU值,则需要进行IP分片 return ip_fragment(sk, skb, ip_finish_output2); return ip_finish_output2(sk, skb); //直接发送报文 }
    static int ip_finish_output_gso(struct net *net, struct sock *sk,
                                     struct sk_buff *skb, unsigned int mtu)
     {
             netdev_features_t features;
             struct sk_buff *segs;
             int ret = 0;
     
             /* Slowpath -  GSO segment length is exceeding the dst MTU.
              *
              * This can happen in two cases:
              * 1) TCP GRO packet, DF bit not set
              * 2) skb arrived via virtio-net, we thus get TSO/GSO skbs directly
              * from host network stack.
              */
             features = netif_skb_features(skb);
             segs = skb_gso_segment(skb, features & ~NETIF_F_GSO_MASK); #这里最终会调用到 UDP 的 gso_segment 回调函数进行 UDP GSO 分段
             if (IS_ERR_OR_NULL(segs)) {
                     kfree_skb(skb);
                     return -ENOMEM;
             }
     
             consume_skb(skb);
     
             do {
                     struct sk_buff *nskb = segs->next;
                     int err;
     
                     segs->next = NULL;
                     err = ip_fragment(net, sk, segs, mtu, ip_finish_output2); #需要的话,再进行 IP 分片,因为 UDP GSO 是按照 MSS 进行,MSS 还是有可能超过 IP 分段所使用的宿主机物理网卡 MTU 的 
                     if (err && ret == 0)
                             ret = err;
                     segs = nskb;
             } while (segs);
     
             return ret;
     }
    复制代码
    • 在函数 static int ip_finish_output_gso(struct net *net, struct sock *sk,  struct sk_buff *skb, unsigned int mtu) 中能看到,首先按照 MSS 做 GSO,然后在调用 ip_fragment 做 IP 分片。可见,在通常情况下(虚机 TCP MSS 要比物理网卡 MTU 小),只做 UDP GSO 分段,IP 分片是不需要做的;只有在特殊情况下 (虚机 TCP MSS 超过了宿主机物理网卡 MTU),IP 分片才会做。 

    这是 UDP 层所注册的 gso 回调函数:

    复制代码
    static const struct net_offload udpv4_offload = {
        .callbacks = {
            .gso_segment = udp4_ufo_fragment,
            .gro_receive  =    udp4_gro_receive,
            .gro_complete =    udp4_gro_complete,
        },
    };
    复制代码

    它的实现在这里:

    复制代码
    static struct sk_buff *__skb_udp_tunnel_segment(struct sk_buff *skb, netdev_features_t features,
        struct sk_buff *(*gso_inner_segment)(struct sk_buff *skb, netdev_features_t features), __be16 new_protocol)
    {
        .../* segment inner packet. */ #先调用内层的 分段函数进行分段
        enc_features = skb->dev->hw_enc_features & netif_skb_features(skb);
        segs = gso_inner_segment(skb, enc_features);
        ...
        skb = segs;
        do { #执行 UDP GSO 分段
            struct udphdr *uh;
            int len;
    
            skb_reset_inner_headers(skb);
            skb->encapsulation = 1;
    
            skb->mac_len = mac_len;
    
            skb_push(skb, outer_hlen);
            skb_reset_mac_header(skb);
            skb_set_network_header(skb, mac_len);
            skb_set_transport_header(skb, udp_offset);
            len = skb->len - udp_offset;
            uh = udp_hdr(skb);
            uh->len = htons(len);
            ...
            skb->protocol = protocol;
        } while ((skb = skb->next));
    out:
        return segs;
    }
    
    struct sk_buff *skb_udp_tunnel_segment(struct sk_buff *skb, netdev_features_t features, bool is_ipv6)
    {
        ...switch (skb->inner_protocol_type) { #计算内层的分片方法
        case ENCAP_TYPE_ETHER: #感觉 vxlan 的 GSO 应该是走这个分支,相当于是将 VXLAN 所封装的二层帧当做 payload 来分段,而不是将包含 VXLAN Header 的部分来分
            protocol = skb->inner_protocol;
            gso_inner_segment = skb_mac_gso_segment;
            break;
        case ENCAP_TYPE_IPPROTO:
            offloads = is_ipv6 ? inet6_offloads : inet_offloads;
            ops = rcu_dereference(offloads[skb->inner_ipproto]);
            if (!ops || !ops->callbacks.gso_segment)
                goto out_unlock;
            gso_inner_segment = ops->callbacks.gso_segment;
            break;
        default:
            goto out_unlock;
        }
    
        segs = __skb_udp_tunnel_segment(skb, features, gso_inner_segment,
                        protocol);
        ...
        return segs; #返回分片好的seg list
    }
    复制代码

    这里比较有疑问的是,VXLAN 没有定义 gso_segment 回调函数,这导致有可能在 UDP GSO 分段里面没有完整的 VXLAN Header。这需要进一步研究。原因可能是在 inner segment 那里,分段是将 UDP 所封装的二层帧当做 payload 来分段,因此,VXLAN Header 就会保持在每个分段中。

    (4)可见,在整个过程中,有客户机上 TCP 协议层设置的 skb_shinfo(skb)->gso_size 始终保持不变为 MSS,因此,在网卡中最终所做的针对 UDP GSO 数据报的 GSO 分片所依据的分片的长度还是根据 skb_shinfo(skb)->gso_size 的值即 TCP MSS。

     

    vxlan收包处理过程

    openvswitch vxlan收包过程如下

    默认情况下发给4789端口的udp数据包,会在内核态呗截取,交给vxlan_rcv处理,vxlan_rcv该函数负责解封装然后将数据包挂入gcells

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    0xffffffff8156efa0 : __napi_schedule+0x0/0x50 [kernel]  //触发软中断

    0xffffffffa045d67b : vxlan_rcv+0x99b/0xb00 [vxlan]

    0xffffffff815e2818 : udp_queue_rcv_skb+0x1f8/0x4f0 [kernel]

    0xffffffff815e355a : __udp4_lib_rcv+0x54a/0x880 [kernel]

    0xffffffff815e3dfa : udp_rcv+0x1a/0x20 [kernel]

    0xffffffff815b1584 : ip_local_deliver_finish+0xb4/0x1f0 [kernel]

    0xffffffff815b1869 : ip_local_deliver+0x59/0xd0 [kernel]

    0xffffffff815b120a : ip_rcv_finish+0x8a/0x350 [kernel]

    0xffffffff815b1b96 : ip_rcv+0x2b6/0x410 [kernel]

    0xffffffff81570062 : __netif_receive_skb_core+0x582/0x800 [kernel]

    0xffffffff815702f8 : __netif_receive_skb+0x18/0x60 [kernel]

    0xffffffff81570380 : netif_receive_skb_internal+0x40/0xc0 [kernel]

    0xffffffff81571498 : napi_gro_receive+0xd8/0x130 [kernel]

    0xffffffffa00472fc : e1000_clean_rx_irq+0x2ac/0x4f0 [e1000]

    0xffffffffa0047d31 : e1000_clean+0x281/0x8f0 [e1000]

    0xffffffff81570b20 : net_rx_action+0x170/0x380 [kernel]

    0xffffffff8108f63f : __do_softirq+0xef/0x280 [kernel]

    0xffffffff8169919c : call_softirq+0x1c/0x30 [kernel]

    0xffffffff8102d365 : do_softirq+0x65/0xa0 [kernel]

    0xffffffff8108f9d5 : irq_exit+0x115/0x120 [kernel]

    软中断出发时候net_rx_action 会处理调用gro_cell_poll从gcells中取出skb进行消耗最终调用__netif_receive_skb_core下的ovs_vport_receive将数据包送给openvswitch流程

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    0xffffffffa043ea40 : ovs_vport_receive+0x0/0xd0 [openvswitch]

    0xffffffffa043fc8e : netdev_frame_hook+0xde/0x160 [openvswitch]

    0xffffffff8156fcc2 : __netif_receive_skb_core+0x1e2/0x800 [kernel]

    0xffffffff815702f8 : __netif_receive_skb+0x18/0x60 [kernel]

    0xffffffff81570380 : netif_receive_skb_internal+0x40/0xc0 [kernel]

    0xffffffff81571498 : napi_gro_receive+0xd8/0x130 [kernel]

    0xffffffffa045a30a : gro_cell_poll+0x7a/0xc0 [vxlan]

    0xffffffff81570b20 : net_rx_action+0x170/0x380 [kernel]

    0xffffffff8108f63f : __do_softirq+0xef/0x280 [kernel]

    0xffffffff8169919c : call_softirq+0x1c/0x30 [kernel]

    0xffffffff8102d365 : do_softirq+0x65/0xa0 [kernel]

    0xffffffff8108f9d5 : irq_exit+0x115/0x120 [kernel]

    0xffffffff81699d38 : do_IRQ+0x58/0xf0 [kernel]

    0xffffffff8168eded : ret_from_intr+0x0/0x15 [kernel]

    数据包送给openvswitch流程在openvswitch内部处理过程和无差别,因为此时数据包已经是解过封装了。所以该数据包会发给namespace left

    该数据包会呗放入到CPU队列中等待left namespace协议栈读取消耗

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    0xffffffff8156f130 : enqueue_to_backlog+0x0/0x170 [kernel]

    0xffffffff8156f2e5 : netif_rx_internal+0x45/0x120 [kernel]

    0xffffffff8156f3de : dev_forward_skb+0x1e/0x30 [kernel]

    0xffffffffa03a34ba : veth_xmit+0x2a/0x60 [veth]

    0xffffffff8156f8a1 : dev_hard_start_xmit+0x171/0x3b0 [kernel]

    0xffffffff81572656 : __dev_queue_xmit+0x466/0x570 [kernel]

    0xffffffff81572770 : dev_queue_xmit+0x10/0x20 [kernel]

    0xffffffffa03881d4 : ovs_vport_send+0x44/0xb0 [openvswitch]

    0xffffffffa037a300 : do_output.isra.31+0x40/0x150 [openvswitch]

    0xffffffffa037b74d : do_execute_actions+0x73d/0x890 [openvswitch]

    0xffffffffa037b8e1 : ovs_execute_actions+0x41/0x130 [openvswitch]

    0xffffffffa037e929 : ovs_packet_cmd_execute+0x2c9/0x2f0 [openvswitch]

    0xffffffff815a6d5a : genl_family_rcv_msg+0x20a/0x430 [kernel]

    0xffffffff815a7011 : genl_rcv_msg+0x91/0xd0 [kernel]

    0xffffffff815a4f89 : netlink_rcv_skb+0xa9/0xc0 [kernel]

    0xffffffff815a54b8 : genl_rcv+0x28/0x40 [kernel]

    0xffffffff815a467d : netlink_unicast+0xed/0x1b0 [kernel]

    0xffffffff815a4a5e : netlink_sendmsg+0x31e/0x690 [kernel]

    0xffffffff81555ef0 : sock_sendmsg+0xb0/0xf0 [kernel]

    0xffffffff81556799 : ___sys_sendmsg+0x3a9/0x3c0 [kernel]

    namespace left协议栈收到该数包发现是发给本机接口的数据包,直接回复icmp reply

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    0xffffffff815e8040 : icmp_rcv+0x0/0x380 [kernel]

    0xffffffff815b1584 : ip_local_deliver_finish+0xb4/0x1f0 [kernel]

    0xffffffff815b1869 : ip_local_deliver+0x59/0xd0 [kernel]

    0xffffffff815b120a : ip_rcv_finish+0x8a/0x350 [kernel]

    0xffffffff815b1b96 : ip_rcv+0x2b6/0x410 [kernel]

    0xffffffff81570062 : __netif_receive_skb_core+0x582/0x800 [kernel]

    0xffffffff815702f8 : __netif_receive_skb+0x18/0x60 [kernel]

    0xffffffff8157159e : process_backlog+0xae/0x170 [kernel]

    0xffffffff81570b20 : net_rx_action+0x170/0x380 [kernel]

    0xffffffff8108f63f : __do_softirq+0xef/0x280 [kernel]

    0xffffffff8169919c : call_softirq+0x1c/0x30 [kernel]

    0xffffffff8102d365 : do_softirq+0x65/0xa0 [kernel]

    0xffffffff8108e894 : local_bh_enable+0x94/0xa0 [kernel]

    0xffffffffa037e930 : ovs_packet_cmd_execute+0x2d0/0x2f0 [openvswitch]

    0xffffffff815a6d5a : genl_family_rcv_msg+0x20a/0x430 [kernel]

    0xffffffff815a7011 : genl_rcv_msg+0x91/0xd0 [kernel]

    0xffffffff815a4f89 : netlink_rcv_skb+0xa9/0xc0 [kernel]

    0xffffffff815a54b8 : genl_rcv+0x28/0x40 [kernel]

    0xffffffff815a467d : netlink_unicast+0xed/0x1b0 [kernel]

    0xffffffff815a4a5e : netlink_sendmsg+0x31e/0x690 [kernel]

    vxlan发包过程

    因为最终数据包从openvswitch侧发给了vxlan口,vxlan口会调用dev_hard_start_xmit将数据包发送出去,因为是vxlan口所以需要对数据包进行封装,很显然封装的过程具体实现细节

    发生在udp_tunnel_xmit_skb 和 iptunnel_xmit函数中,最后调用ip_local_out_sk将封装好的数据包当成本机数据包发出去,当然此时二层、三次转发查找路由的过程,都是借用的本机发包的流程了,这里就不再详细说明了

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    0xffffffff815fbfc0 : iptunnel_xmit+0x0/0x1a0 [kernel]

    0xffffffffa02b12b3 : udp_tunnel_xmit_skb+0xe3/0x100 [udp_tunnel]

    0xffffffffa039a253 : vxlan_xmit_one+0x7e3/0xb60 [vxlan]

    0xffffffffa039b81f : vxlan_xmit+0x41f/0xce0 [vxlan]

    0xffffffff8156f8a1 : dev_hard_start_xmit+0x171/0x3b0 [kernel]

    0xffffffff81572656 : __dev_queue_xmit+0x466/0x570 [kernel]

    0xffffffff81572770 : dev_queue_xmit+0x10/0x20 [kernel]

    0xffffffffa03881d4 : ovs_vport_send+0x44/0xb0 [openvswitch]

    0xffffffffa037a300 : do_output.isra.31+0x40/0x150 [openvswitch]

    0xffffffffa037b74d : do_execute_actions+0x73d/0x890 [openvswitch]

    0xffffffffa037b8e1 : ovs_execute_actions+0x41/0x130 [openvswitch]

    0xffffffffa037f445 : ovs_dp_process_packet+0x85/0x110 [openvswitch]

    0xffffffffa0387aac : ovs_vport_receive+0x6c/0xd0 [openvswitch]

    0xffffffffa0388c8e : netdev_frame_hook+0xde/0x160 [openvswitch]

    0xffffffff8156fcc2 : __netif_receive_skb_core+0x1e2/0x800 [kernel]

    0xffffffff815702f8 : __netif_receive_skb+0x18/0x60 [kernel]

    0xffffffff8157159e : process_backlog+0xae/0x170 [kernel]

    0xffffffff81570b20 : net_rx_action+0x170/0x380 [kernel]

    0xffffffff8108f63f : __do_softirq+0xef/0x280 [kernel]

    0xffffffff8169919c : call_softirq+0x1c/0x30 [kernel]

    vlxan数据包UDP端口的选择

    从代码实现来看,应该是根据vxlan封装前的源目的ip和端口进行hash获取的UDP发送端口,细节后续再研究

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    static inline __be16 udp_flow_src_port(struct net *net, struct sk_buff *skb,
    int min, int max, bool use_eth)
    {
    u32 hash;

    if (min >= max) {
    /* Use default range */
    inet_get_local_port_range(net, &min, &max);
    }

    hash = skb_get_hash(skb);
    if (unlikely(!hash) && use_eth) {
    /* Can't find a normal hash, caller has indicated an Ethernet
    * packet so use that to compute a hash.
    */
    hash = jhash(skb->data, 2 * ETH_ALEN,
    (__force u32) skb->protocol);
    }

    /* Since this is being sent on the wire obfuscate hash a bit
    * to minimize possbility that any useful information to an
    * attacker is leaked. Only upper 16 bits are relevant in the
    * computation for 16 bit port value.
    */
    hash ^= hash << 16;

    return htons((((u64) hash * (max - min)) >> 32) + min);
    }
  • 相关阅读:
    关于bind named.conf.options
    MASM 16位汇编程序几种典型的格式
    About GCC
    WebForms UnobtrusiveValidationMode 需要“jQuery”ScriptResourceMapping。
    Linux系统下的shutdown命令用于安全的关闭/重启计算机
    TreeView.ImageSet 属性
    python seaborn
    python neo4j
    Impala与Hive的比较
    pandas.resample()
  • 原文地址:https://www.cnblogs.com/dream397/p/14506319.html
Copyright © 2011-2022 走看看