产品战略专家梁宁确认出席AICon北京站,分享AI时代下的商业逻辑与产品需求 了解详情
写点什么

Docker 源码分析(七):Docker Container 网络 (上)

  • 2015-01-24
  • 本文字数:7961 字

    阅读完需:约 26 分钟

1. 前言 (什么是 Docker Container)

如今,Docker 技术大行其道,大家在尝试以及玩转 Docker 的同时,肯定离不开一个概念,那就是“容器”或者“Docker Container”。那么我们首先从实现的角度来看看“容器”或者“Docker Container”到底为何物。

逐渐熟悉 Docker 之后,大家肯定会深深得感受到:应用程序在 Docker Container 内部的部署与运行非常便捷,只要有 Dockerfile,应用一键式的部署运行绝对不是天方夜谭; Docker Container 内运行的应用程序可以受到资源的控制与隔离,大大满足云计算时代应用的要求。毋庸置疑,Docker 的这些特性,传统模式下应用是完全不具备的。然而,这些令人眼前一亮的特性背后,到底是谁在“作祟”,到底是谁可以支撑 Docker 的这些特性?不知道这个时候,大家是否会联想到强大的 Linux 内核。

其实,这很大一部分功能都需要归功于 Linux 内核。那我们就从 Linux 内核的角度来看看 Docker 到底为何物,先从 Docker Container 入手。关于 Docker Container,体验过的开发者第一感觉肯定有两点:内部可以跑应用(进程),以及提供隔离的环境。当然,后者肯定也是工业界称之为“容器”的原因之一。

既然 Docker Container 内部可以运行进程,那么我们先来看 Docker Container 与进程的关系,或者容器与进程的关系。首先,我提出这样一个问题供大家思考“容器是否可以脱离进程而存在”。换句话说,能否创建一个容器,而这个容器内部没有任何进程。

可以说答案是否定的。既然答案是否定的,那说明不可能先有容器,然后再有进程,那么问题又来了,“容器和进程是一起诞生,还是先有进程再有容器呢?”可以说答案是后者。以下将慢慢阐述其中的原因。

阐述问题“容器是否可以脱离进程而存在”的原因前,相信大家对于以下的一段话不会持有异议:通过 Docker 创建出的一个 Docker Container 是一个容器,而这个容器提供了进程组隔离的运行环境。那么问题在于,容器到底是通过何种途径来实现进程组运行环境的“隔离”。这时,就轮到 Linux 内核技术隆重登场了。

说到运行环境的“隔离”,相信大家肯定对 Linux 的内核特性 namespace 和 cgroup 不会陌生。namespace 主要负责命名空间的隔离,而 cgroup 主要负责资源使用的限制。其实,正是这两个神奇的内核特性联合使用,才保证了 Docker Container 的“隔离”。那么,namespace 和 cgroup 又和进程有什么关系呢?问题的答案可以用以下的次序来说明:

(1) 父进程通过 fork 创建子进程时,使用 namespace 技术,实现子进程与其他进程(包含父进程)的命名空间隔离;

(2) 子进程创建完毕之后,使用 cgroup 技术来处理子进程,实现进程的资源使用限制;

(3) 系统在子进程所处 namespace 内部,创建需要的隔离环境,如隔离的网络栈等;

(4) namespace 和 cgroup 两种技术都用上之后,进程所处的“隔离”环境才真正建立,这时“容器”才真正诞生!

从 Linux 内核的角度分析容器的诞生,精简的流程即如以上 4 步,而这 4 个步骤也恰好巧妙的阐述了 namespace 和 cgroup 这两种技术和进程的关系,以及进程与容器的关系。进程与容器的关系,自然是:容器不能脱离进程而存在,先有进程,后有容器。然而,大家往往会说到“使用 Docker 创建 Docker Container(容器),然后在容器内部运行进程”。对此,从通俗易懂的角度来讲,这完全可以理解,因为“容器”一词的存在,本身就较为抽象。如果需要更为准确的表述,那么可以是:“使用 Docker 创建一个进程,为这个进程创建隔离的环境,这样的环境可以称为 Docker Container(容器),然后再在容器内部运行用户应用进程。”当然,笔者的本意不是想否定很多人对于 Docker Container 或者容器的认识,而是希望和读者一起探讨 Docker Container 底层技术实现的原理。

对于 Docker Container 或者容器有了更加具体的认识之后,相信大家的眼球肯定会很快定位到 namespace 和 cgroup 这两种技术。Linux 内核的这两种技术,竟然能起到如此重大的作用,不禁为之赞叹。那么下面我们就从 Docker Container 实现流程的角度简要介绍这两者。

首先讲述一下 namespace 在容器创建时的用法,首先从用户创建并启动容器开始。当用户创建并启动容器时,Docker Daemon 会 fork 出容器中的第一个进程 A(暂且称为进程 A,也就是 Docker Daemon 的子进程)。Docker Daemon 执行 fork 时,在 clone 系统调用阶段会传入 5 个参数标志 CLONE_NEWNS、CLONE_NEWUTS、CLONE_NEWIPC、CLONE_NEWPID 和 CLONE_NEWNET(目前 Docker 1.2.0 还没有完全支持 user namespace)。Clone 系统调用一旦传入了这些参数标志,子进程将不再与父进程共享相同的命名空间(namespace),而是由 Linux 为其创建新的命名空间(namespace),从而保证子进程与父进程使用隔离的环境。另外,如果子进程 A 再次 fork 出子进程 B 和 C,而 fork 时没有传入相应的 namespace 参数标志,那么此时子进程 B 和 C 将会与 A 共享同一个命令空间(namespace)。如果 Docker Daemon 再次创建一个 Docker Container,容器内第一个进程为 D,而 D 又 fork 出子进程 E 和 F,那么这三个进程也会处于另外一个新的 namespace。两个容器的 namespace 均与 Docker Daemon 所在的 namespace 不同。Docker 关于 namespace 的简易示意图如下:

图 1.1 Docker 中 namespace 示意图

再说起 cgroup,大家都知道可以使用 cgroup 为进程组做资源的控制。与 namespace 不同的是,cgroup 的使用并不是在创建容器内进程时完成的,而是在创建容器内进程之后再使用 cgroup,使得容器进程处于资源控制的状态。换言之,cgroup 的运用必须要等到容器内第一个进程被真正创建出来之后才能实现。当容器内进程被创建完毕,Docker Daemon 可以获知容器内进程的 PID 信息,随后将该 PID 放置在 cgroup 文件系统的指定位置,做相应的资源限制。

可以说 Linux 内核的 namespace 和 cgroup 技术,实现了资源的隔离与限制。那么对于这种隔离与受限的环境,是否还需要配置其他必需的资源呢。这回答案是肯定的,网络栈资源就是在此时为容器添加。当为容器进程创建完隔离的运行环境时,发现容器虽然已经处于一个隔离的网络环境(即新的 network namespace),但是进程并没有独立的网络栈可以使用,如独立的网络接口设备等。此时,Docker Daemon 会将 Docker Container 所需要的资源一一为其配备齐全。网络方面,则需要按照用户指定的网络模式,配置 Docker Container 相应的网络资源。

2.Docker Container 网络分析内容安排

Docker Container 网络篇将从源码的角度,分析 Docker Container 从无到有的过程中,Docker Container 网络创建的来龙去脉。Docker Container 网络创建流程可以简化如下图:

图 2.1 Docker Container 网络创建流程图

Docker Container 网络篇分析的主要内容有以下 5 部分:

(1) Docker Container 的网络模式;

(2) Docker Client 配置容器网络;

(3) Docker Daemon 创建容器网络流程;

(4) execdriver 网络执行流程;

(5) libcontainer 实现内核态网络配置。

Docker Container 网络创建过程中,networkdriver 模块使用并非是重点,故分析内容中不涉及 networkdriver。这里不少读者肯定会有疑惑。需要强调的是,networkdriver 在 Docker 中的作用:第一,为 Docker Daemon 创建网络环境的时候,初始化 Docker Daemon 的网络环境(详情可以查看《Docker 源码分析》系列第六篇),比如创建 docker0 网桥等;第二,为 Docker Container 分配 IP 地址,为 Docker Container 做端口映射等。而与 Docker Container 网络创建有关的内容极少,只有在桥接模式下,为 Docker Container 的网络接口设备分配一个可用 IP 地址。

本文为《Docker 源码分析》系列第七篇——Docker Container 网络(上)。

3.Docker Container 网络模式

正如在上文提到的,Docker 可以为 Docker Container 创建隔离的网络环境,在隔离的网络环境下,Docker Container 独立使用私有网络。相信很多的 Docker 开发者也是体验过 Docker 这方面的网络特性。

其实,Docker 除了可以为 Docker Container 创建隔离的网络环境之外,同样有能力为 Docker Container 创建共享的网络环境。换言之,当开发者需要 Docker Container 与宿主机或者其他容器网络隔离时,Docker 可以满足这样的需求;而当开发者需要 Docker Container 与宿主机或者其他容器共享网络时,Docker 同样可以满足这样的需求。另外,Docker 还可以不为 Docker Container 创建网络环境。

总结 Docker Container 的网络,可以得出 4 种不同的模式:bridge 桥接模式、host 模式、other container 模式和 none 模式。以下初步介绍 4 中不同的网络模式。

3.1 bridge 桥接模式

Docker Container 的 bridge 桥接模式可以说是目前 Docker 开发者最常使用的网络模式。Brdige 桥接模式为 Docker Container 创建独立的网络栈,保证容器内的进程组使用独立的网络环境,实现容器间、容器与宿主机之间的网络栈隔离。另外,Docker 通过宿主机上的网桥 (docker0) 来连通容器内部的网络栈与宿主机的网络栈,实现容器与宿主机乃至外界的网络通信。

Docker Container 的 bridge 桥接模式可以参考下图:

图 3.1 Docker Container Bridge 桥接模式示意图

Bridge 桥接模式的实现步骤主要如下:

(1) Docker Daemon 利用 veth pair 技术,在宿主机上创建两个虚拟网络接口设备,假设为 veth0 和 veth1。而 veth pair 技术的特性可以保证无论哪一个 veth 接收到网络报文,都会将报文传输给另一方。

(2) Docker Daemon 将 veth0 附加到 Docker Daemon 创建的 docker0 网桥上。保证宿主机的网络报文可以发往 veth0;

(3) Docker Daemon 将 veth1 添加到 Docker Container 所属的 namespace 下,并被改名为 eth0。如此一来,保证宿主机的网络报文若发往 veth0,则立即会被 eth0 接收,实现宿主机到 Docker Container 网络的联通性;同时,也保证 Docker Container 单独使用 eth0,实现容器网络环境的隔离性。

Bridge 桥接模式,从原理上实现了 Docker Container 到宿主机乃至其他机器的网络连通性。然而,由于宿主机的 IP 地址与 veth pair 的 IP 地址均不在同一个网段,故仅仅依靠 veth pair 和 namespace 的技术,还不足以是宿主机以外的网络主动发现 Docker Container 的存在。为了使得 Docker Container 可以让宿主机以外的世界感知到容器内部暴露的服务,Docker 采用 NAT(Network Address Translation,网络地址转换)的方式,让宿主机以外的世界可以主动将网络报文发送至容器内部。

具体来讲,当 Docker Container 需要暴露服务时,内部服务必须监听容器 IP 和端口号 port_0,以便外界主动发起访问请求。由于宿主机以外的世界,只知道宿主机 eth0 的网络地址,而并不知道 Docker Container 的 IP 地址,哪怕就算知道 Docker Container 的 IP 地址,从二层网络的角度来讲,外界也无法直接通过 Docker Container 的 IP 地址访问容器内部应用。因此,Docker 使用 NAT 方法,将容器内部的服务监听的端口与宿主机的某一个端口 port_1 进行“绑定”。

如此一来,外界访问 Docker Container 内部服务的流程为:

(1) 外界访问宿主机的 IP 以及宿主机的端口 port_1;

(2) 当宿主机接收到这样的请求之后,由于 DNAT 规则的存在,会将该请求的目的 IP(宿主机 eth0 的 IP)和目的端口 port_1 进行转换,转换为容器 IP 和容器的端口 port_0;

(3) 由于宿主机认识容器 IP,故可以将请求发送给 veth pair;

(4) veth pair 的 veth0 将请求发送至容器内部的 eth0,最终交给内部服务进行处理。

使用 DNAT 方法,可以使得 Docker 宿主机以外的世界主动访问 Docker Container 内部服务。那么 Docker Container 如何访问宿主机以外的世界呢。以下简要分析 Docker Container 访问宿主机以外世界的流程:

(1) Docker Container 内部进程获悉宿主机以外服务的 IP 地址和端口 port_2,于是 Docker Container 发起请求。容器的独立网络环境保证了请求中报文的源 IP 地址为容器 IP(即容器内部 eth0),另外 Linux 内核会自动为进程分配一个可用源端口(假设为 port_3);

(2) 请求通过容器内部 eth0 发送至 veth pair 的另一端,到达 veth0,也就是到达了网桥(docker0)处;

(3) docker0 网桥开启了数据报转发功能(/proc/sys/net/ipv4/ip_forward),故将请求发送至宿主机的 eth0 处;

(4) 宿主机处理请求时,使用 SNAT 对请求进行源地址 IP 转换,即将请求中源地址 IP(容器 IP 地址)转换为宿主机 eth0 的 IP 地址;

(5) 宿主机将经过 SNAT 转换后的报文通过请求的目的 IP 地址(宿主机以外世界的 IP 地址)发送至外界。

在这里,很多人肯定会问:对于 Docker Container 内部主动发起对外的网络请求,当请求到达宿主机进行 SNAT 处理后发给外界,当外界响应请求时,响应报文中的目的 IP 地址肯定是 Docker 宿主机的 IP 地址,那响应报文回到宿主机的时候,宿主机又是如何转给 Docker Container 的呢?关于这样的响应,由于 port_3 端口并没有在宿主机上做相应的 DNAT 转换,原则上不会被发送至容器内部。为什么说对于这样的响应,不会做 DNAT 转换呢。原因很简单,DNAT 转换是针对容器内部服务监听的特定端口做的,该端口是供服务监听使用,而容器内部发起的请求报文中,源端口号肯定不会占用服务监听的端口,故容器内部发起请求的响应不会在宿主机上经过 DNAT 处理。

其实,这一环节的内容是由 iptables 规则来完成,具体的 iptables 规则如下:

复制代码
iptables -I FORWARD -o docker0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT

这条规则的意思是,在宿主机上发往 docker0 网桥的网络数据报文,如果是该数据报文所处的连接已经建立的话,则无条件接受,并由 Linux 内核将其发送到原来的连接上,即回到 Docker Container 内部。

以上便是 Docker Container 中 bridge 桥接模式的简要介绍。可以说,bridger 桥接模式从功能的角度实现了两个方面:第一,让容器拥有独立、隔离的网络栈;第二,让容器和宿主机以外的世界通过 NAT 建立通信。

然而,bridge 桥接模式下的 Docker Container 在使用时,并非为开发者包办了一切。最明显的是,该模式下 Docker Container 不具有一个公有 IP,即和宿主机的 eth0 不处于同一个网段。导致的结果是宿主机以外的世界不能直接和容器进行通信。虽然 NAT 模式经过中间处理实现了这一点,但是 NAT 模式仍然存在问题与不便,如:容器均需要在宿主机上竞争端口,容器内部服务的访问者需要使用服务发现获知服务的外部端口等。另外 NAT 模式由于是在三层网络上的实现手段,故肯定会影响网络的传输效率。

3.2 host 模式

Docker Container 中的 host 模式与 bridge 桥接模式有很大的不同。最大的区别当属,host 模式并没有为容器创建一个隔离的网络环境。而之所以称之为 host 模式,是因为该模式下的 Docker Container 会和 host 宿主机共享同一个网络 namespace,故 Docker Container 可以和宿主机一样,使用宿主机的 eth0,实现和外界的通信。换言之,Docker Container 的 IP 地址即为宿主机 eth0 的 IP 地址。

Docker Container 的 host 网络模式可以参考下图:

图 3.2 Docker Container host 网络模式示意图

上图最左侧的 Docker Container,即采用了 host 网络模式,而其他两个 Docker Container 依然沿用 brdige 桥接模式,两种模式同时存在于宿主机上并不矛盾。

Docker Container 的 host 网络模式在实现过程中,由于不需要额外的网桥以及虚拟网卡,故不会涉及 docker0 以及 veth pair。上文 namespace 的介绍中曾经提到,父进程在创建子进程时,如果不使用 CLONE_NEWNET 这个参数标志,那么创建出的子进程会与父进程共享同一个网络 namespace。Docker 就是采用了这个简单的原理,在创建进程启动容器的过程中,没有传入 CLONE_NEWNET 参数标志,实现 Docker Container 与宿主机共享同一个网络环境,即实现 host 网络模式。

可以说,Docker Container 的网络模式中,host 模式是 bridge 桥接模式很好的补充。采用 host 模式的 Docker Container,可以直接使用宿主机的 IP 地址与外界进行通信,若宿主机的 eth0 是一个公有 IP,那么容器也拥有这个公有 IP。同时容器内服务的端口也可以使用宿主机的端口,无需额外进行 NAT 转换。当然,有这样的方便,肯定会损失部分其他的特性,最明显的是 Docker Container 网络环境隔离性的弱化,即容器不再拥有隔离、独立的网络栈。另外,使用 host 模式的 Docker Container 虽然可以让容器内部的服务和传统情况无差别、无改造的使用,但是由于网络隔离性的弱化,该容器会与宿主机共享竞争网络栈的使用;另外,容器内部将不再拥有所有的端口资源,原因是部分端口资源已经被宿主机本身的服务占用,还有部分端口已经用以 bridge 网络模式容器的端口映射。

3.3 other container 模式

Docker Container 的 other container 网络模式是 Docker 中一种较为特别的网络的模式。之所以称为“other container 模式”,是因为这个模式下的 Docker Container,会使用其他容器的网络环境。之所以称为“特别”,是因为这个模式下容器的网络隔离性会处于 bridge 桥接模式与 host 模式之间。Docker Container 共享其他容器的网络环境,则至少这两个容器之间不存在网络隔离,而这两个容器又与宿主机以及除此之外其他的容器存在网络隔离。

Docker Container 的 other container 网络模式可以参考下图:

图 3.3 Docker Container other container 网络模式示意图

上图右侧的 Docker Container 即采用了 other container 网络模式,它能使用的网络环境即为左侧 Docker Container brdige 桥接模式下的网络。

Docker Container 的 other container 网络模式在实现过程中,不涉及网桥,同样也不需要创建虚拟网卡 veth pair。完成 other container 网络模式的创建只需要两个步骤:

(1) 查找 other container(即需要被共享网络环境的容器)的网络 namespace;

(2) 将新创建的 Docker Container(也是需要共享其他网络的容器)的 namespace,使用 other container 的 namespace。

Docker Container 的 other container 网络模式,可以用来更好的服务于容器间的通信。

在这种模式下的 Docker Container 可以通过 localhost 来访问 namespace 下的其他容器,传输效率较高。虽然多个容器共享网络环境,但是多个容器形成的整体依然与宿主机以及其他容器形成网络隔离。另外,这种模式还节约了一定数量的网络资源。但是需要注意的是,它并没有改善容器与宿主机以外世界通信的情况。

3.4 none 模式

Docker Container 的第四种网络模式是 none 模式。顾名思义,网络环境为 none,即不为 Docker Container 任何的网络环境。一旦 Docker Container 采用了 none 网络模式,那么容器内部就只能使用 loopback 网络设备,不会再有其他的网络资源。

可以说 none 模式为 Docker Container 做了极少的网络设定,但是俗话说得好“少即是多”,在没有网络配置的情况下,作为 Docker 开发者,才能在这基础做其他无限多可能的网络定制开发。这也恰巧体现了 Docker 设计理念的开放。

4. 作者介绍

孙宏亮, DaoCloud 初创团队成员,软件工程师,浙江大学 VLIS 实验室应届研究生。读研期间活跃在 PaaS 和 Docker 开源社区,对 Cloud Foundry 有深入研究和丰富实践,擅长底层平台代码分析,对分布式平台的架构有一定经验,撰写了大量有深度的技术博客。2014 年末以合伙人身份加入 DaoCloud 团队,致力于传播以 Docker 为主的容器的技术,推动互联网应用的容器化步伐。邮箱: allen.sun@daocloud.io

5. 下期预告

下期内容为:Docker 源码分析(八):Docker Container 网络(下)


感谢郭蕾对本文的审校和策划。

给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ )或者腾讯微博( @InfoQ )关注我们,并与我们的编辑和其他读者朋友交流。

2015-01-24 01:3210330

评论

发布
暂无评论
发现更多内容

Argo CD 可观测性最佳实践

观测云

ArgoCD

2023 IoTDB Summit:Dr. Julian Feinauer《Apache IoTDB 在德国工业和关键基础设施中的应用》

Apache IoTDB

使用NGINX在Kubernetes中对TCP和UDP流量进行负载均衡设置教程

百度搜索:蓝易云

nginx Linux Kubernetes TCP udp

智慧工地建设与低代码开发: 优化建筑行业的效率与安全

不在线第一只蜗牛

低代码 项目开发 智慧工地 数智转型

EMQ 发布MQTT over QUIC 白皮书:下一代车联网消息传输标准协议

新消费日报

软件供应链安全继续强化:SBOM清单基座规范SBOMit启动制订

sender_is_sender

软件开发生命周期 软件供应链安全 软件物料清单(SBOM) in-toto

轻量级UML建模工具 Astah Professional mac注册激活版 附详细安装教程

南屿

UML建模 Astah Professional破解版 astah professional怎么用

Cheetah3D 8:对 Apple Silicon 的原生支持 Metal API 的本机支持

南屿

动画 渲染 3d建模 Cheetah3D注册机 Cheetah3D 8新功能

微服务架构与低代码开发:加速应用开发的完美结合

快乐非自愿限量之名

架构 微服务 低代码 应用开发

Bartender 4 下载 Mac菜单栏管理 v4.2.25 支持m1 m2

南屿

Bartender 4 Bartender5 Bartender破解版 Mac软件下载站

resolume arena破解版 附安装教程 Mac电脑VJ调试软件 兼容M1

南屿

Mac软件 苹果电脑 Resolume Arena 7破解版 VJ调试 Resolume Arena 安装教程

EOS系统合约总体介绍

BSN研习社

区块链 EOS

小游戏选型(一):游戏化设计助力直播间互动和营收

音视频开发_AIZ

音视频开发 小游戏 小游戏开发 小游戏运营 直播间

对接50+快递商,快递鸟电子面单API助力商家多平台批量打单发货

快递鸟

快递物流 快递

车内语音识别数据在智能驾驶中的价值与应用

来自四九城儿

车内语音识别技术:智能驾驶的革新之源

来自四九城儿

eudic欧路词典下载 mac翻译软件 v4.5.9 增强激活版 支持m1 m2

南屿

Mac 翻译软件 欧路词典 Eudic Eudic欧路词典破解版 英汉翻译

这么做,开发打造高水平国际体育赛事直播观看平台

软件开发-梦幻运营部

车内语音识别数据在智能驾驶中的应用与挑战

来自四九城儿

车内语音识别技术在智能驾驶中的应用与前景

来自四九城儿

8个可替代Visio的绘图软件推荐!每一款都堪称神器。

彭宏豪95

效率工具 流程图 在线白板 绘图软件 Visio

istio工作原理

百度搜索:蓝易云

Linux 运维 istio 云服务器 Sidecar

硬负载均衡和软负载均衡有什么区别?

百度搜索:蓝易云

云计算 Linux 负载均衡 运维 云服务器

爆火《幻兽帕鲁》被指用AI缝合宝可梦,开发者自曝传奇经历:是人类的奇迹

Openlab_cosmoplat

车内语音识别技术:智能驾驶的核心要素

来自四九城儿

直播预告|原生 vs 跨端,聊聊鸿蒙应用开发的真实感受

小红书技术REDtech

鸿蒙 前端 开发 跨端开发 小红书

幻兽帕鲁来啦!京东云召唤你一键开服,快来私服联机

京东科技开发者

MQTT over QUIC 白皮书:下一代车联网消息传输标准协议

EMQ映云科技

车联网 mqtt QUIC QUIC协议 mqtt broker

史上最全知识图谱建模实践(上):本体结构与语义解耦

可信AI进展

深度学习 nlp 知识图谱 NLP 大模型

左耳听风 - 技术领导力「读书打卡 day 17」

Java 工程师蔡姬

读书笔记 程序员 个人成长 职业发展 技术领导力

车内语音识别技术:重塑智能驾驶的未来

来自四九城儿

Docker源码分析(七):Docker Container网络 (上)_开源_孙宏亮_InfoQ精选文章