作者:炎寻

过去一年,ARMS 基于 eBPF 技术打造了 Kubernetes 监控,提供多语言无侵入的应用性能,系统性能,网络性能观测能力,并发布 Kubernetes 问题排查全景图,验证了 eBPF 技术的有效性。eBPF 技术和生态发展很好,未来前景广大,作为该技术的实践者,本文目标是通过回答 7 个核心问题介绍 eBPF 技术本身,为大家解开 eBPF 的面纱。

关注【阿里云云原生】公众号,后台回复关键词【K8s全景图】获取全景图高清下载地址!

eBPF 是什么

eBPF 是一个能够在内核运行沙箱程序的技术,提供了一种在内核事件和用户程序事件发生时安全注入代码的机制,使得非内核开发人员也可以对内核进行控制。随着内核的发展,eBPF 逐步从最初的数据包过滤扩展到了网络、内核、安全、跟踪等,而且它的功能特性还在快速发展中,早期的 BPF 被称为经典 BPF,简称 cBPF,正是这种功能扩展,使得现在的 BPF 被称为扩展 BPF,简称 eBPF。

eBPF 的应用场景是什么?

网络优化

eBPF 兼具高性能和高可扩展特性,使得其成为网络方案中网络包处理的优选方案:

  • 高性能

JIT 编译器提供近乎内核本地代码的执行效率。

  • 高可扩展

在内核的上下文里,可以快速地增加协议解析和路由策略。

故障诊断

eBPF 通过 kprobe,tracepoints 跟踪机制兼具内核和用户的跟踪能力,这种端到端的跟踪能力可以快速进行故障诊断,与此同时 eBPF 支持以更加高效的方式透出 profiling 的统计数据,而不需要像传统系统需要将大量的采样数据透出,使得持续地实时 profiling 成为可能。

安全控制

eBPF 可以看到所有系统调用,所有网络数据包和 socket 网络操作,一体化结合进程上下文跟踪,网络操作级别过滤,系统调用过滤,可以更好地提供安全控制。

性能监控

相比于传统的系统监控组件比如 sar,只能提供静态的 counters 和 gauges,eBPF 支持可编程地动态收集和边缘计算聚合自定义的指标和事件,极大地提升了性能监控的效率和想象空间。

eBPF 为什么会出现?

eBPF 的出现本质上是为了解决内核迭代速度慢和系统需求快速变化的矛盾,在 eBPF 领域常用的一个例子是 eBPF 相对于 Linux Kernel 类似于 Javascript 相对于 HTML,突出的是可编程性。一般来说可编程性的支持通常会带来一些新的问题,比如内核模块其实也是为了解决这个问题,但是他没有提供很好的边界,导致内核模块会影响内核本身的稳定性,在不同的内核版本需要做适配等。eBPF 采用以下策略,使得其成为一种安全高效地内核可编程技术:

  • 安全

eBPF 程序必须被验证器校验通过后才能执行,且不能包含无法到达的指令;eBPF 程序不能随意调用内核函数,只能调用在 API 中定义的辅助函数;eBPF 程序栈空间最多只有 512 字节,想要更大的存储,就必须要借助映射存储。

  • 高效

借助即时编译器(JIT),且因为 eBPF 指令依然运行在内核中,无需向用户态复制数据,大大提高了事件处理的效率。

  • 标准

通过 BPF Helpers,BTF,PERF MAP 提供标准的接口和数据模型供开发者使用。

  • 功能强大

eBPF 不仅扩展了寄存器的数量,引入了全新的 BPF 映射存储,还在 4.x 内核中将原本单一的数据包过滤事件逐步扩展到了内核态函数、用户态函数、跟踪点、性能事件(perf_events)以及安全控制等领域。

eBPF 怎么用?

5 个步骤

1、使用 C 语言开发一个 eBPF 程序;

即插桩点触发事件时要调用的 eBPF 沙箱程序,该程序会在内核态运行。

2、借助 LLVM 把 eBPF 程序编译成 BPF 字节码;

eBPF 程序编译成 BPF 字节码,用于后续在 eBPF 虚拟机内验证并运行。

3、通过 bpf 系统调用,把 BPF 字节码提交给内核;

在用户态通过 bpf 系统,将 BPF 字节码加载到内核。

4、内核验证并运行 BPF 字节码,并把相应的状态保存到 BPF 映射中;

内核验证 BPF 字节码安全,并且确保对应事件发生时调用正确的 eBPF 程序,如果有状态需要保存,则写入对应 BPF 映射中,比如监控数据就可以写到 BPF 映射中。

5、用户程序通过 BPF 映射查询 BPF 字节码的运行状态。

用户态通过查询 BPF 映射的内容,获取字节码运行的状态,比如获取抓取到的监控数据。

一个完整的 eBPF 程序,通常包含用户态和内核态两部分:用户态程序需要通过 BPF 系统调用跟内核进行交互,进而完成 eBPF 程序加载、事件挂载以及映射创建和更新等任务;而在内核态中,eBPF 程序也不能任意调用内核函数,而是需要通过 BPF 辅助函数完成所需的任务。尤其是在访问内存地址的时候,必须要借助 bpf_probe_read 系列函数读取内存数据,以确保内存的安全和高效访问。在 eBPF 程序需要大块存储时,我们还需要根据应用场景,引入特定类型的 BPF 映射,并借助它向用户空间的程序提供运行状态的数据。

eBPF 程序分类和使用场景

bpftool feature probe | grep program_type

以上命令可以查看系统支持的 eBPF 程序类型,一般有如下类型:

eBPF program_type socket_filter is available
eBPF program_type kprobe is available
eBPF program_type sched_cls is available
eBPF program_type sched_act is available
eBPF program_type tracepoint is available
eBPF program_type xdp is available
eBPF program_type perf_event is available
eBPF program_type cgroup_skb is available
eBPF program_type cgroup_sock is available
eBPF program_type lwt_in is available
eBPF program_type lwt_out is available
eBPF program_type lwt_xmit is available
eBPF program_type sock_ops is available
eBPF program_type sk_skb is available
eBPF program_type cgroup_device is available
eBPF program_type sk_msg is available
eBPF program_type raw_tracepoint is available
eBPF program_type cgroup_sock_addr is available
eBPF program_type lwt_seg6local is available
eBPF program_type lirc_mode2 is NOT available
eBPF program_type sk_reuseport is available
eBPF program_type flow_dissector is available
eBPF program_type cgroup_sysctl is available
eBPF program_type raw_tracepoint_writable is available
eBPF program_type cgroup_sockopt is available
eBPF program_type tracing is available
eBPF program_type struct_ops is available
eBPF program_type ext is available
eBPF program_type lsm is available

具体可参考:

https://elixir.bootlin.com/linux/v5.13/source/include/linux/bpf_types.h

主要是分为 3 大使用场景:

  • 跟踪

tracepoint, kprobe, perf_event 等,主要用于从系统中提取跟踪信息,进而为监控、排错、性能优化等提供数据支撑。

  • 网络

xdp, sock_ops, cgroup_sock_addr , sk_msg 等,主要用于对网络数据包进行过滤和处理,进而实现网络的观测、过滤、流量控制以及性能优化等各种丰富的功能,这里可以丢包,重定向。

cilium 基本用了所有的 hook 点。

  • 安全和其他

lsm,用于安全,其他还有 flow_dissector, lwt_in 都是一些不怎么常用的,不再赘述。

eBPF 的最佳实践是什么?

寻找内核的插桩点

从前面可以看出来 eBPF 程序本身并不困难,困难的是为其寻找合适的事件源来触发运行。对于监控和诊断领域来说,跟踪类 eBPF 程序的事件源包含 3 类:内核函数(kprobe)、内核跟踪点(tracepoint)或性能事件(perf_event)。此时有 2 个问题需要回答:

1、内核中都有哪些内核函数、内核跟踪点或性能事件?

  • 使用调试信息获取内核函数、内核跟踪点
sudo ls /sys/kernel/debug/tracing/events
  • 使用 bpftrace 获取内核函数、内核跟踪点
# 查询所有内核插桩和跟踪点
sudo bpftrace -l# 使用通配符查询所有的系统调用跟踪点
sudo bpftrace -l 'tracepoint:syscalls:*'# 使用通配符查询所有名字包含"open"的跟踪点
sudo bpftrace -l '*open*'
  • 使用 perf list 获取性能事件
sudo perf list tracepoint

2、对于内核函数和内核跟踪点,在需要跟踪它们的传入参数和返回值的时候,又该如何查询这些数据结构的定义格式呢?

  • 使用调试信息获取
sudo cat /sys/kernel/debug/tracing/events/syscalls/sys_enter_openat/format

使用 bpftrace 获取

sudo bpftrace -lv tracepoint:syscalls:sys_enter_openat

具体如何使用以上信息,请参考 bcc。

寻找应用的插桩点

1、如何查询用户进程的跟踪点?

  • 静态编译语言通过-g 编译选项保留调试信息,应用程序二进制会包含 DWARF(Debugging With Attributed Record Format),有了调试信息,可以通过 readelf、objdump、nm 等工具,查询可用于跟踪的函数、变量等符号列表
# 查询符号表
readelf -Ws /usr/lib/x86_64-linux-gnu/libc.so.6# 查询USDT信息
readelf -n /usr/lib/x86_64-linux-gnu/libc.so.6
  • 使用 bpftrace
# 查询uprobe
bpftrace -l 'uprobe:/usr/lib/x86_64-linux-gnu/libc.so.6:*'# 查询USDT
bpftrace -l 'usdt:/usr/lib/x86_64-linux-gnu/libc.so.6:*'

uprobe 是基于文件的。当文件中的某个函数被跟踪时,除非对进程 PID 进行了过滤,默认所有使用到这个文件的进程都会被插桩。

上面说的是静态编译语言,他和内核的跟踪类似,应用程序的符号信息可以存放在 ELF 二进制文件中,也可以以单独文件的形式,放到调试文件中;而内核的符号信息除了可以存放到内核二进制文件中之外,还会以 /proc/kallsyms 和 /sys/kernel/debug 等形式暴露到用户空间。

对于非静态编译语言来说,主要是两种:

1、解释型语言

使用类似编译型语言应用程序的跟踪点查询方法,查询它们在解释器层面的 uprobe 和 USDT 跟踪点,如何将解释器层面的行为和应用行为关联需要相关语言的专家来分析。

2、即时编译型语言

这类语言的应用源代码会先编译为字节码,再由即时编译器(JIT)编译为机器码执行,还会有大量的优化,跟踪难度很大,同解释型编程语言类似,uprobe 和 USDT 跟踪只能用在即时编译器上,从即时编译器的跟踪点参数里面获取最终应用程序的函数信息。找出即时编译器的跟踪点同应用程序运行之间的关系需要相关语言的专家来分析。

可以参考 BCC 的应用程序跟踪,用户进程的跟踪,本质上是通过断点去执行 uprobe 处理程序。虽然内核社区已经对 BPF 做了很多的性能调优,跟踪用户态函数(特别是锁争用、内存分配之类的高频函数)还是有可能带来很大的性能开销。因此,我们在使用 uprobe 时,应该尽量避免跟踪高频函数。

具体如何使用以上信息,请参考:

https://github.com/iovisor/bcc/blob/master/docs/reference_guide.md#events--arguments****

关联问题与插桩点

一个理想的状态是所有问题都清楚应当观察那些插桩点,但是这个要求技术人员对端到端的软件栈细节都了解十分透彻,一个更加合理的方法是二八法则,将软件栈数据流的最核心的 80%脉络抓住,保障出现问题一定会在这个脉络被发现即可。此时再使用内核栈和用户栈来查看具体的调用栈即可发现核心问题,比如说发现了网络在丢包,但是不知道为什么丢,此时我们知道网络丢包一定会调用 kfree_skb 内核函数,那么我们可以通过:

sudo bpftrace -e 'kprobe:kfree_skb /comm=="<your comm>"/ {printf("kstack: %s\n", kstack);}'

发现该函数的调用栈:

kstack: kfree_skb+1 udpv6_destroy_sock+66 sk_common_release+34 udp_lib_close+9 inet_release+75 inet6_release+49 __sock_release+66 sock_close+21 __fput+159 ____fput+14 task_work_run+103 exit_to_user_mode_loop+411 exit_to_user_mode_prepare+187 syscall_exit_to_user_mode+23 do_syscall_64+110 entry_SYSCALL_64_after_hwframe+68

那么就可以回溯上面的函数,看看他们具体是哪一行在什么条件下调用的,就能够定位到问题。这个方法不仅可以定位问题,也可以用于加深对内核调用的理解,比如:

bpftrace -e 'tracepoint:net:* { printf("%s(%d): %s %s\n", comm, pid, probe, kstack()); }'

可以查看所有网络相关的跟踪点及其调用栈。

eBPF 的实现原理是什么?

5 个模块

eBPF 在内核主要由 5 个模块协作:

1、BPF Verifier(验证器)

确保 eBPF 程序的安全。验证器会将待执行的指令创建为一个有向无环图(DAG),确保程序中不包含不可达指令;接着再模拟指令的执行过程,确保不会执行无效指令,这里通过和个别同学了解到,这里的验证器并无法保证 100%的安全,所以对于所有 BPF 程序,都还需要严格的监控和评审。

2、BPF JIT

将 eBPF 字节码编译成本地机器指令,以便更高效地在内核中执行。

3、多个 64 位寄存器、一个程序计数器和一个 512 字节的栈组成的存储模块

用于控制 eBPF 程序的运行,保存栈数据,入参与出参。

4、BPF Helpers(辅助函数)

提供了一系列用于 eBPF 程序与内核其他模块进行交互的函数。这些函数并不是任意一个 eBPF 程序都可以调用的,具体可用的函数集由 BPF 程序类型决定。注意,eBPF 里面所有对入参,出参的修改都必须符合 BPF 规范,除了本地变量的变更,其他变化都应当使用 BPF Helpers 完成,如果 BPF Helpers 不支持,则无法修改。

bpftool feature probe

通过以上命令可以看到不同类型的 eBPF 程序可以运行哪些 BPF Helpers。

5、BPF Map & context

用于提供大块的存储,这些存储可被用户空间程序用来进行访问,进而控制 eBPF 程序的运行状态。

bpftool feature probe | grep map_type

通过以上命令可以看到系统支持哪些类型的 map。

3 个动作

先说下重要的系统调用 bpf:

int bpf(int cmd, union bpf_attr *attr, unsigned int size);

这里 cmd 是关键,attr 是 cmd 的参数,size 是参数大小,所以关键是看 cmd 有哪些:

// 5.11内核
enum bpf_cmd {
BPF_MAP_CREATE,
BPF_MAP_LOOKUP_ELEM,
BPF_MAP_UPDATE_ELEM,
BPF_MAP_DELETE_ELEM,
BPF_MAP_GET_NEXT_KEY,
BPF_PROG_LOAD,
BPF_OBJ_PIN,
BPF_OBJ_GET,
BPF_PROG_ATTACH,
BPF_PROG_DETACH,
BPF_PROG_TEST_RUN,
BPF_PROG_GET_NEXT_ID,
BPF_MAP_GET_NEXT_ID,
BPF_PROG_GET_FD_BY_ID,
BPF_MAP_GET_FD_BY_ID,
BPF_OBJ_GET_INFO_BY_FD,
BPF_PROG_QUERY,
BPF_RAW_TRACEPOINT_OPEN,
BPF_BTF_LOAD,
BPF_BTF_GET_FD_BY_ID,
BPF_TASK_FD_QUERY,
BPF_MAP_LOOKUP_AND_DELETE_ELEM,
BPF_MAP_FREEZE,
BPF_BTF_GET_NEXT_ID,
BPF_MAP_LOOKUP_BATCH,
BPF_MAP_LOOKUP_AND_DELETE_BATCH,
BPF_MAP_UPDATE_BATCH,
BPF_MAP_DELETE_BATCH,
BPF_LINK_CREATE,
BPF_LINK_UPDATE,
BPF_LINK_GET_FD_BY_ID,
BPF_LINK_GET_NEXT_ID,
BPF_ENABLE_STATS,
BPF_ITER_CREATE,
BPF_LINK_DETACH,
BPF_PROG_BIND_MAP,
};

最核心的就是 PROG,MAP 相关的 cmd,就是程序加载和映射处理。

1、程序加载

调用 BPF_PROG_LOAD cmd,会将 BPF 程序加载到内核,但 eBPF 程序并不像常规的线程那样,启动后就一直运行在那里,它需要事件触发后才会执行。这些事件包括系统调用、内核跟踪点、内核函数和用户态函数的调用退出、网络事件,等等,所以需要第 2 个动作。

2、绑定事件

b.attach_kprobe(event="xxx", fn_name="yyy")

以上就是将特定的事件绑定到特定的 BPF 函数,实际实现原理如下:

(1)借助 bpf 系统调用,加载 BPF 程序之后,会记住返回的文件描述符;

(2)通过 attach 操作知道对应函数类型的事件编号;

(3)根据 attach 的返回值调用 perf_event_open 创建性能监控事件;

(4)通过 ioctl 的 PERF_EVENT_IOC_SET_BPF 命令,将 BPF 程序绑定到性能监控事件。

3、映射操作

通过 MAP 相关的 cmd,控制 MAP 增删,然后用户态基于该 MAP 与内核状态进行交互。

eBPF 的发展现状?

内核支持

建议>=4.14

生态

eBPF 的生态自下而上的情况如下:

1、基础设施

支持 eBPF 基础能力的发展。

  • Linux Kernal
  • LLVM\

2、开发工具集

主要是用于加载,编译,调试 eBPF 程序,不同语言有不同的开发工具集:

  • Go

  • https://github.com/cilium/ebpf

  • https://github.com/aquasecurity/libbpfgo

  • C/C++

  • https://github.com/libbpf/libbpf

3、eBPF 应用

  • bcc

    https://github.com/iovisor/bcc

提供一套开发工具和脚本。

  • bpftrace

    https://github.com/iovisor/bpftrace

基于 bcc,提供一个脚本语言。

  • cilium

    https://github.com/cilium/cilium

网络优化和安全

  • Falco

    https://github.com/falcosecurity/falco

网络安全

  • Katran
    https://github.com/facebookincubator/katran

高性能 4 层负载均衡

  • Hubble

    https://github.com/cilium/hubble

可观测

  • Kindling

    https://github.com/CloudDectective-Harmonycloud/kindling

可观测

  • Pixie

    https://github.com/pixie-io/pixie

可观测

  • kubectl trace

    https://github.com/iovisor/kubectl-trace

调度 bpftrace 脚本

  • L3AF

    https://github.com/l3af-project/l3afd

分布式环境下启动和管理 eBPF 程序的平台

  • ply

    https://github.com/iovisor/ply

动态 linux trace

  • Tracee

    https://github.com/aquasecurity/tracee

Linux 运行时安全监测

4、跟踪生态的网站

  • https://ebpf.io/projects

  • https://github.com/zoidbergwill/awesome-ebpf

写在最后

用好 eBPF 的前提是对软件栈的理解

通过上面的介绍,相信大家对 eBPF 已经有了足够的理解,eBPF 提供的只是一个框架和机制,核心还是需要用 eBPF 的人对软件栈的理解,找到合适的插桩点,能够和应用问题进行关联。

eBPF 的杀手锏是全覆盖,无侵入,可编程

1、全覆盖

内核,应用程序插桩点全覆盖。

2、无侵入

不需要修改任何被 hook 的代码。

3、可编程

动态下发 eBPF 程序,边缘动态执行指令,动态聚合分析。

团队信息

阿里云可观测团队,覆盖前端监控、应用监控、容器监控、Prometheus、链路追踪、智能告警、运维可视化等多个技术领域及产品,沉淀阿里云可观测在不同行业、不同技术场景的可观测解决方案与最佳实践。

阿里云 Kubernetes 监控是一套基于 eBPF 技术,针对 Kubernetes 集群开发的一站式无侵入式可观测性产品,基于 Kubernetes 集群下的指标、应用链路、日志和事件,旨在为 IT 开发运维人员提供整体的可观测性方案。

介绍:

https://help.aliyun.com/document_detail/260777.html

接入:

https://help.aliyun.com/document_detail/251852.html

深入浅出 eBPF|你要了解的 7 个核心问题相关推荐

  1. 深入浅出 eBPF: (Linux/Kernel/XDP/BCC/BPFTrace/Cillium)

    [BPF入门系列-1]eBPF 技术简介 | 深入浅出 eBPF[BPF入门系列-1]eBPF 技术简介https://www.ebpf.top/post/bpf_intro_blog/ 目录 eBP ...

  2. 【深入浅出Java并发编程指南】「难点 - 核心 - 遗漏」线程状态流转及生命周期的技术指南(知识点串烧)

    前提介绍 本章主要介绍相关线程声明周期的转换机制以及声明周期的流转关系以及相关AQS的实现和相关的基本原理,配合这相关官方文档的中英文互译的介绍. 线程状态流转及生命周期 当线程被创建并启动以后,它既 ...

  3. eBPF动手实践系列一:解构内核源码eBPF样例编译过程

    作者:闻茂泉 他山之石 了解和掌握纯c语言的ebpf编译和使用,有助于我们加深对于eBPF技术原理的进一步掌握,也有助于开发符合自己业务需求的高性能的ebpf程序.目前常见和主流的纯c语言的ebpf编 ...

  4. 初识 eBPF(功能、原理、及一些应用)

    非常棒的一些材料 当eBPF遇上Linux内核网络 什么是 eBPF? 待看 基于eBPF监控和排查云原生环境中的磁盘IO性能问题 深度解密基于 eBPF 的 Kubernetes 问题排查全景图 e ...

  5. ebpf之bcc程序入门

    原理 参考:高效入门eBPF_哔哩哔哩_bilibili 环境安装 参考:https://github.com/iovisor/bcc/blob/master/INSTALL.md#ubuntu-so ...

  6. 瑞雪时晴,不亦快哉 ——图灵十一月月刊

    瑞雪时晴,不亦快哉 --图灵十一月月刊 11 月初北京下了20 多 年来最早的一场雪,雪景喜人,又如甘霖,解了北方多日苦旱.本期图灵月刊题图正是被传为"书圣"王羲之当今存世唯一真迹 ...

  7. C++游戏开发需要阅读的书籍

    如果要自学游戏程序开发的话,可以看看下面的,呵呵. 游戏开发资料(PDF书都是中文版的,非英文,很多是本人自己扫描制作,从未网上发布过,所以独家啦):   1.Gamebryo 2.2游戏引擎(盛大. ...

  8. YoloV5代码详细解读

    本文重点描述开源YoloV5代码实现的细节,不会对YoloV5的整体思路进行介绍,整体思路可以参考江大白的博客 江大白:深入浅出Yolo系列之Yolov3&Yolov4&Yolov5& ...

  9. yolov4的全面详解

    一. 前言 作者AlexeyAB大神!  YOLOv4  拥有43.5%mAP+65FPS  ,达到了精度速度最优平衡, 作者团队:Alexey Bochkovskiy&中国台湾中央研究院 论 ...

  10. Yolo系列知识点梳理(Yolov1-v5)

    文章目录 1 概述 2 Yolo系列模型 2.1 基石 - Yolov1 2.1.1 Yolov1的网络结构 2.1.2 Yolov1的feature map 2.1.3 Yolov1的训练 2.1. ...

最新文章

  1. vue2 切换路由时 页面滚动到顶部 用游览器返回时 记住上页的位置
  2. 排序算法(五):快速排序
  3. TinyBERT搜索: 比BERT快10倍,小20倍
  4. 屏蔽storm ui的kill功能
  5. android 获取存储卡,Android获取存储卡路径的方式
  6. MacOs桌面自动被打乱的原因
  7. 接口成员的特点 java 1614101629
  8. Java一步到位!彻底了解JDK、JRE、JVM分别是什么及它们之间的联系
  9. 用生活案例讲述:erp系统是什么?有哪些作用?
  10. oracle 范鑫_【企业信息化研究所】TF—SWUFE Oracle Club招新宣讲,只为和你相遇
  11. 【Python】利用Python爬虫实现网页图片批量下载
  12. 微信小程序开发之——用户登录-搭建开发者服务器(2)
  13. 如何修改Maven仓库地址为阿里云仓库
  14. 基于Vue+Express+Mysql开发的手机端电影购票系统(附源码)
  15. KiTTY及cnKiTTY配置文件kitty.ini的简单中文注释
  16. 七年交易经验,倾囊分享中长线交易秘诀
  17. 【数学建模学习】matlab实现评价模型——层次分析法(AHP)
  18. Nginx上传大图片或者大文件失败
  19. 赛博朋克 “故障风”按钮
  20. 物流团队怎么做阿米巴电商?

热门文章

  1. 淘宝客优惠券领取微信小程序前台和后台源码
  2. window10+cuda+cudnn下载
  3. 非常好的Oracle基础教程
  4. 有关 VS winform 开发问题
  5. 解决OptiSystem安装、使用过程中遇到的问题
  6. First_Day_NingShanFeng之品优购电商系统开发
  7. SAP学习记__物料管理(MM)模块__采购入库冲销、退货
  8. jQuery新浪微博表情插件教程
  9. iOS 虚拟GPS 就是为了玩pokemon go 哈哈哈哈
  10. 如何下载网页中的flash SWF文件