├── .gitignore ├── LICENSE ├── README.md └── book ├── ch02 └── s2.0.md ├── ch05 ├── images │ └── 编译链接01.jpg ├── s5.0.md ├── s5.1.md ├── s5.2.md ├── s5.3.md └── s5.4.md └── ch07 ├── s7.0.md ├── s7.1.md ├── s7.2.md ├── s7.3.md ├── s7.4.md └── s7.5.md /.gitignore: -------------------------------------------------------------------------------- 1 | .gitconfig 2 | _build/ 3 | material 4 | 5 | notes/ 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Neutron 网络基础 2 | 3 | ## 说明 4 | 5 | 本书记录了笔者在工作及日常学习中的一些学习笔记以及经验。这是一本关于Neutron的基础书籍,或许其最终的篇幅会的很长,但还是希望大家把他当成一本基础书籍来翻阅。下面是本书在编写时候的一些说明: 6 | 7 | * 严格性不足。笔者希望能编写一本内容严格的书籍出来,但由于能力、时间的原因,目前不能保证书中的内容百分百准确。虽然或许书中的某些内容存在错误,但笔者会保证这些内容对于Neutron的基础来说是足够了的。当读者在日常的工作中要用到某个知识点的时候,本书能起到一个引路人的作用,帮助读者更快更好的去掌握这个知识点。 8 | * 注重广度而非深度。由于本书的定位是Neutron的基础知识,所以笔者会更加偏向于基础知识的广度而非深度。 9 | 10 | ## 写作周期 11 | 12 | 本书会分为三个阶段进行编写: 13 | 14 | 1. 初稿,完成大体框架。不包含图片 15 | 2. 对初稿的语言、内容进行加工,补充图片 16 | 3. 定稿 17 | 18 | 目前处于第一个阶段。 19 | 20 | ## 错误纠正 21 | 22 | 由于笔者水平有限,书中难免有错误。如果大家发现了问题欢迎大家指出,可以: 23 | 24 | * 提issue 25 | * 发送错误内容邮件给tianhuan@bingotree.cn 26 | 27 | ## 目录&进度 28 | 29 | 注意:目前处于第一阶段,下面的目录可能会有微调。 30 | 31 | ``` 32 | 第一章 本书介绍 33 | 第二章 Neutron的部署、运维、高可用 34 | 常用命令(ip/iptables/tc & qdisc/tcpdump/route/brctl/veth) 35 | namespace简介及用法 36 | ovs简介及常用命令 37 | libvirt/qemu中的网络 38 | Neutron部署 39 | Neutron日常维护及监控 40 | Neutron高可用(各个组件高可用探讨、部署方法) 41 | Neutron安全 42 | Neutron升级 43 | 第三章 Neutron实现 44 | 开发环境的搭建 45 | 如何共享代码 46 | 基础类库 47 | 重要基础类库(stevedore) 48 | 非重要基础类库 49 | Big Picture 50 | ML2 51 | 设计思想 52 | ovs流表操作练习 53 | 实现分析 54 | DHCP Agent 55 | 设计思想 56 | 实现分析 57 | L3 Agent 58 | 设计思想 59 | 路由相关操作练习 60 | 实现分析 61 | Metadata Agent 62 | 设计思想 63 | 实现分析 64 | SRIOV Agent 65 | 设计思想 66 | 实现分析 67 | Service Chain 68 | 设计思想 69 | 实现分析 70 | 其它 71 | ARP防毒化 72 | Dragon Flow 73 | 思科ACI 74 | ... 75 | 第四章 其它组件与Neutron的交互 76 | Nova 77 | Trove 78 | ... 79 | 第五章 底层网络基础 80 | 5.1内核网络相关基础知识 # 一期完成 81 | sk_buff数据结构 # 一期完成 82 | net_device数据结构 # 一期完成 83 | 硬件中断&module机制 # 一期完成 84 | softirq # 一期完成 85 | proc文件系统 # 一期完成 86 | 5.2协议栈中的收包/发包 # 一期完成 87 | PCIe设备驱动的加载 # 一期完成 88 | 收包路径 # 一期完成 89 | 发包路径 # 一期完成 90 | lo/veth的实现 # 一期完成 91 | 多队列/RSS/RPS/RFS # 一期完成 92 | LSO/LRO/GSO/GRO/TSO/USO # 一期完成 93 | DPDK # 一期完成 94 | 5.3网络namespace的实现 # 一期完成 95 | 5.4Linux Bridge的实现 # 一期完成 96 | 5.5OVS的实现 97 | 第六章 SDN 98 | 第七章 容器中的网络 # 一期完成 99 | 容器 or 传统虚拟化? # 一期完成 100 | Docker # 一期完成 101 | Kubernetes # 一期完成 102 | libnetwork # 一期完成 103 | Neutron与容器 # 一期完成 104 | 第八章 后记 105 | ``` 106 | 107 | -------------------------------------------------------------------------------- /book/ch02/s2.0.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QthCN/neutron-basic-book/a397cd86c7fa9f406fcb6c91f74bf73c8f1be6fb/book/ch02/s2.0.md -------------------------------------------------------------------------------- /book/ch05/images/编译链接01.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QthCN/neutron-basic-book/a397cd86c7fa9f406fcb6c91f74bf73c8f1be6fb/book/ch05/images/编译链接01.jpg -------------------------------------------------------------------------------- /book/ch05/s5.0.md: -------------------------------------------------------------------------------- 1 | # 底层网络基础 2 | 3 | 前面两章我们主要是将注意力放在了Neutron的实现及设计思想,以及与OpenStack中其余组件的交互上,大家如果对这两章有了很好的理解那么相信一定已经有能力可以对Neutron的源码做出很大的贡献或定制化了。在第二章的时候小秦列了一些基础的网络知识,这些知识可以帮助大家在日常运维的时候对网络相关的操作以及Neutron中网络相关的一些组件有一定的运维基础。但是这些对于深入理解我们的网络还不够。在小秦的日常工作中,组内的一些同事有很多的时间是用于性能优化或者一些性能疑难问题的排查,大家都在想办法压榨网卡的性能,想办法提高网络节点的处理包的能力。同时,对于小秦来说,对于一个系统如何运行的好奇之心也让我花了一些时间去了解系统的一些底层实现。比如一个数据包到达网卡后会内核会做些什么操作让这个数据包传到上层?因此本章就会将注意力放在这些底层的东西上。内容主要有: 4 | 5 | - 内核网络相关基础知识 6 | - 协议栈中的收包/发包 7 | - 网络Namespace的实现 8 | - Linux Bridge的实现 9 | - OVS的实现 10 | 11 | 由于这些基础的实现一般是C语言实现的,C本身的语法没有Python那么容易阅读,因此小秦会尽量多用图片和文字的方式帮助大家理解上面的内容。所以,如果对C没有基础也不用担心。 12 | 13 | 对于一个数据包,在二层可能叫做数据帧,在三层才会叫数据包。本章中除非有特定的含义(此时小秦会指出),否则一般都会叫做数据包。 14 | 15 | 另外,由于水平有限,上面的这些内容小秦也只是入了个门。如果存在错误希望大家指出。多谢! 16 | -------------------------------------------------------------------------------- /book/ch05/s5.1.md: -------------------------------------------------------------------------------- 1 | ## 内核网络相关基础知识 2 | 3 | 这一章主要是为了后面的章节做准备的,内容包括: 4 | 5 | - sk_buff数据结构 6 | - net_device数据结构 7 | - 硬件中断&module机制 8 | - softirq 9 | - proc文件系统 10 | 11 | 关于这些方面的小秦所发现的最好的参考资料是《Understanding Linux Network Internals》。强烈建议每个对Linux内核中网络有兴趣的同学读下这本书。本书中与底层网络相关的资料很多都是参考着这本书而来的。由于本书的目标还是基础,因此对于很多内容都只是做个入门的介绍,如果读者感兴趣可以参考其它的书籍或内核代码进行深入的学习。 12 | 13 | ### sk_buff数据结构 14 | 15 | sk_buff在内核的地位简单的说就是:它代表了一个数据包。 16 | 17 | 首先来个大致的介绍让大家感受下它的重要性吧。比如一个网卡收到了一个包,此时大家可以认为这个数据包对于网卡来说只是一段电信号或光信号,网卡收到这个包后,会先保存在网卡本地,然后会告诉内核说:“内核君,我这有个包,快来拿”。内核于是就会去取这个包,既然是内核去取,那么这个包肯定到了内核后就要放到内存中的某个数据结构里了,这个数据结果对于内核来说就是sk_buff。所以我们可以认为此时这个sk_buff数据结构包含了这个数据包的所有内容。内核得到了这个sk_buff后,接下来做的事情就是让它走协议栈,我们就把协议栈当成是一个有n层楼的大厦、sk_buff是一个携带了传单想要在楼顶散发传单的小伙好了。于是sk_buff这个小伙就开始爬大厦,可能它能一直爬过所有楼层爬到楼顶,也可能在某一层就因为迷路或者找错大厦之类的原因被原路返回(也就是要下楼了)或者因为非法发传单而被捕了。在它爬楼的过程中每一层楼的楼管都会对其要发的传单做些标注或修改,但不论怎样传单在小伙爬到楼顶散发前都是属于这个小伙的,楼管不管做什么操作也只是在这个小伙的传单上做操作,如果某层的某个居民想了解传单的内容,那么必须去找这个小伙了解。所以可以看到,sk_buff就是我们的数据包在从数据包到达内核以及这个包最终到达上层之间唯一拥有这个数据包内容的结构体。其包含了网卡收到的数据包的全部信息以及协议栈对这个数据包做出的相关附加信息。 18 | 19 | 下面来看下这个sk_buff结构体的几个重要属性,列出的属性中有些属性在后面的章节会成为主角: 20 | 21 | ``` 22 | struct sk_buff { 23 | ...... 24 | struct sk_buff *next; //指向下一个sk_buff 25 | struct sk_buff *prev; //指向前一个sk_buff 26 | 27 | struct sock *sk; //拥有这个sk_buff的socket 28 | 29 | struct net_device *dev; //发送或者接收这个数据包的设备 30 | 31 | unsigned int len; //数据包的真实长度 32 | 33 | unsigned char *head; unsigned char *end; unsigned char *data; unsigned char *tail; 34 | 35 | __u32 priority; //数据包的优先级 36 | 37 | __u8 pkt_type:3; //数据包的类型 38 | __be16 protocol; //驱动程序判定的这个数据包的协议类型 39 | ...... 40 | } 41 | ``` 42 | 43 | 一开始的例子中举例了小伙发传单爬大楼的例子,sk_buff在内核协议栈中就是这样一层一层走协议栈的。第一层可能是链路层(L2),然后爬到IP层(L3),再然后会爬到传输层(L4)。在走协议栈的过程中,sk_buff中的一些属性可能会被修改。先来说下上面列出的几个重要属性。 44 | 45 | next/prev: 46 | 47 | 内核中的所有sk_buff都被串在了一个双向链表里以便检索和管理。这两个指针分别指向双向链表的前者和后者。 48 | 49 | sk: 50 | 51 | 如果一个数据包是发给本机的或者是由本机生成的,那么一般会有上层应用接收或产生这个数据包。写过网络代码的人都知道目前Linux是通过socket来实现网络编程的,这个sk指针就指向了将要接收这个数据包或者是产生了这个数据包的socket。如果这个数据包是走转发路径的呢(比如我本机用来做路由器)?此时这个指针就是NULL。 52 | 53 | dev: 54 | 55 | 我们下面一节就会讲到这个结构体。net_device在内核中代表了一个网络设备,比如我们的eth0、lo设备等。在sk_buff中这个结构体指针指的是发送或接收这个数据包的设备。在我们下面的章节中,net_device这个结构体要远比sk_buff重要的多。 56 | 57 | pkt_type: 58 | 59 | 这个值是根据sk_buff的二层目的地址判定出的数据包的类型。所有的类型可以在include/linux/if_packet.h中找到。 60 | 61 | protocol: 62 | 63 | protocol指的是三层的协议类型。一般是IP、IPv6以及ARP。这个值一般是在收到数据包后,由对应的网卡的驱动程序判断数据包的三层协议类型后设置的。在我们之后将收包流程的时候会看到这个值是如何被驱动设置的。 64 | 65 | head/end/data/tail: 66 | 67 | 这四个指针分别指向了sk_buff中的不同的数据段(可以把sk_buff中的其它熟悉看成是元信息,data或head指向的才是我们真实的数据包内容)。简单的说:head指向了我们的sk_buff的最开头,end指向了我们的sk_buff的最末尾。data指向了我们真正协议报文内容的最开头,tail指向了我们真正协议报文内容的最末尾。为什么会这么复杂呢?原因在于内存的分配、扩容操作是一个比较耗时的操作。在学校网络课的时候网络老师最有可能举的一个协议栈的例子就是邮局送信。我们写好信后,我们得将它放到信封里,信封上贴好邮票写上地址才能投递出去。投递的过程中信连着信封会放在邮局的小卡车上在城市间传输,传送到目的地后收件人会拆开信封,然后才会看到信的内容。对于协议栈也存在这么一个加信封、解信封的过程,在协议栈里信封就是我们的协议头。在内存中如果我们明确知道要分配多大的内存,那么我们直接分配一块连续内存就行了(可以理解这个内存就是我们的信的内容),但是由于我们有增加头部、移除头部(加信封、解信封)的过程,我们势必要在我们申请的内存的头部后者尾部再次申请内存。在已有的一块内存的头部或尾部再次附加一段内存对于内核来说可是一件比较痛苦的事情,所以内核的方法很简单:一次性就申请一大块足够大的内存,这块内存的头就是head指针指向的地方,尾就是end指向的地方。但这块内存中真正有意义的数据由data和tail来标示。比如一开始这块内存只存在TCP的数据,所以data会的指向TCP的头部,此时data和head之间的一大块空闲内存就还空着。然后这个包传到了IP层,IP也要给这个数据包加上IP的头部,于是data指针会往head指针靠近,其会的靠近IP头部的长度这么一块距离。 68 | 69 | len: 70 | 71 | len指的是我们上面说的data指针与tail指针之间的差值,也就是数据包报文目前被协议栈认为的有意义的长度的大小。因此当sk_buff在协议栈之间传递的时候,随着data或tail指针的变化,其长度也会相应变化。 72 | 73 | 74 | 关于sk_buff我们就介绍这么多,对于我们接下来的内容来说,大家最重要的是要记住sk_buff代表了我们的数据包,在内核协议栈中数据包通过sk_buff这个数据结构来在协议栈之间传递。 75 | 76 | 77 | ### net_device数据结构 78 | 79 | 如果我们通过ifconfig或ip link show命令查看本机的网卡,我们可以看到类似如下的输出: 80 | 81 | ``` 82 | [root@dev ~]# ip link show 83 | 1: lo: mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT 84 | link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 85 | 2: enp0s3: mtu 1500 qdisc pfifo_fast state UP mode DEFAULT qlen 1000 86 | link/ether 08:00:27:89:4a:1e brd ff:ff:ff:ff:ff:ff 87 | 3: enp0s8: mtu 1500 qdisc pfifo_fast state UP mode DEFAULT qlen 1000 88 | link/ether 08:00:27:77:2b:ca brd ff:ff:ff:ff:ff:ff 89 | 4: enp0s9: mtu 1500 qdisc pfifo_fast state UP mode DEFAULT qlen 1000 90 | link/ether 08:00:27:66:66:88 brd ff:ff:ff:ff:ff:ff 91 | 5: enp0s10: mtu 1500 qdisc pfifo_fast state UP mode DEFAULT qlen 1000 92 | link/ether 08:00:27:5e:84:f9 brd ff:ff:ff:ff:ff:ff 93 | 6: virbr0: mtu 1500 qdisc noqueue state DOWN mode DEFAULT 94 | link/ether 52:54:00:27:e8:bc brd ff:ff:ff:ff:ff:ff 95 | 7: virbr0-nic: mtu 1500 qdisc pfifo_fast master virbr0 state DOWN mode DEFAULT qlen 500 96 | link/ether 52:54:00:27:e8:bc brd ff:ff:ff:ff:ff:ff 97 | ``` 98 | 99 | 这里我们的ip命令列出了本机的所有网卡,比如lo、enp0s3、virbr0等。其中即有lo这个回环设备,也有enp0s3这个真实的物理设备,还存在virbr0这个网桥。在内核中,这些都是由net_device这个结构体来表示的。一个net_device代表了一个网络设备,包括真实的物理设备、虚拟设备或者网桥等。当一个网络设备被驱动识别后,驱动就会在内核中建立一个net_device数据结构代表这个设备,之后在数据包的发送和接收的时候,这个设备都会作为重要的参数在各个函数之间传递(因此C没有对象的概念......)。我们来看下其重要的一些属性: 100 | 101 | ``` 102 | struct net_device { 103 | ...... 104 | char name[IFNAMSIZ]; //设备名 105 | 106 | int irq; //这个设备注册的中断向量号 107 | 108 | unsigned int flags; //用于表示这个设备的一些说明 109 | 110 | unsigned int mtu; //表示这个设备的mtu大小 111 | unsigned int type; //表示这个设备支持的网络类型,比如以太网 112 | unsigned long state; //表示这个设备的状态 113 | 114 | struct Qdisc *qdisc; //通过tc进行数据包的发送的时候会的用到这个 115 | 116 | const struct net_device_ops *netdev_ops; //这个设备所支持的大部分操作的函数实现 117 | const struct ethtool_ops *ethtool_ops; //这个设备所支持的ethtool操作的函数实现 118 | ...... 119 | } 120 | ``` 121 | 122 | 我们来看下这几个属性代表了什么。 123 | 124 | name: 125 | 126 | name就是我们通过ip link show看到的lo、enp0s3这样的名字。 127 | 128 | irq: 129 | 130 | 在我们下面讲中断的时候会的讲到中断号的作用。简单的理解就是:当外部硬件设备发生了一件事情的时候,会需要某个方式通知内核,而对于内核来说其只会知道目前XXX发生了事情,而XXX就是个数字。于是内核去一个类似于Python字典结构中根据XXX去找对应的XXX的处理函数,然后被动的执行这个函数去处理这个事件。XXX就是我们这里的irq,如果中断没有共享的话这个irq和我们的net_device是唯一对应的,否则驱动程序要判断产生的中断事件是不是其自己所关心的。比如三个设备都是用10这个irq号,此时内核收到了一个中断号为10的事件,于是内核会依次调用这个三个设备的对应中断处理函数,每个函数要判断下这个事件是不是自己要处理的说(简单的说就是这个事件是不是自己设备产生的)。 131 | 132 | flags: 133 | 134 | 我们通过ip link show命令的时候可以看到这个设备的一些状态信息,这些信息就是由flags这个字段标记的(当然还有其他的字段表示其它的一些信息)。比如是否是处于混杂模式等。 135 | 136 | mtu: 137 | 138 | mtu只的是这个设备所能传输的最大报文大小。注意和传输层的最大传输大小区分开来。 139 | 140 | type: 141 | 142 | type表示这个网络设备的类型,比如以太网设备等。 143 | 144 | state: 145 | 146 | state表示这个设备的状态。这个在后面我们会看到这个状态字段会影响我们的数据包能否被发送。比如如果state中__LINK_STATE_XOFF标记位为true的时候这个网卡就不能发送数据包啦。 147 | 148 | qdisc: 149 | 150 | 内核中发送数据包的时候会的走一个叫做tc(traffic control)的系统。简单的说就是一个数据包要被发送的时候,先扔到qdisc中去排队,然后再从qdisc中取出一个包,这个被取出的包才是要被发送的。至于这个包是不是我扔进去的那个包就不知道了,这得开tc配置的是什么算法了。tc在我们之前的章节中有过介绍,在之后看到发包的实现的时候大家就清楚其起作用的时间了。 151 | 152 | netdev_ops: 153 | 154 | 在解释这个之前先说下C的函数指针。在面向对象的语言中或多或少存在接口(interface)的概念。比如我一个Python类Animal,其存在一个方法是run。那么我可以有一个子类Dog继承这个Animal类,然后其必然拥有run这个方法。其可以通过覆盖父类自己实现run,或者直接使用父类的run,但是总之我们知道其肯定有run这个方法。因此通过我们之前文章中讲过的stevedore等方法我们可以动态的加载对应的driver而调用不同的实现。对于C语言来说其语言级别没有直接的接口这种特性,所以其通过函数指针的方式实现类似的功能。比如我们的结构体A拥有ops这个函数指针,然后我们写个注释说这个ops指向的函数会做打印当前主机内存的操作。然后我们在linux上跑这个程序,此时可以实现一个函数show_mem其实现为调用free方法查看内存大小。然后将ops指向这个函数,然后别人调用A的ops的时候就能看到内存大小了。同样的,在windows上我们的free方法就不管用了,于是有人在windows上也实现了一个windows版本的show_mem,然后将A结构体的ops指向这个windows版本的show_mem,此时上层调用A这个结构体的程序不用做任何修改,其调用A的ops方法依然可以正常获取到windows主机的内存大小。在net_device中这种需求就特别强烈了,比如收发包这个操作在网卡层面每家厂商都不一样,但内核的代码肯定不能根据不同的厂商动态变吧,所以内核的代码是通用的,其调用类似于A的ops方法,但是每个驱动可以实现不同的ops,于是对于驱动X,内核调用ops就会执行X的实现,对于驱动Y则内核调用ops就会执行Y的实现。netdev_ops就包含了内核需要的大部分数据包在网卡层面的收取、发送的函数指针。比如:ndo_change_mtu用于修改网卡的mtu,ndo_start_xmit用于发送数据包等。 155 | 156 | ethtool_ops: 157 | 158 | ethtool_ops也是一个包含了大量函数指针的结构体,和netdev_ops的区别在于其主要用于实现ethtool的各种功能。在这里ethtool就是我们的上层应用,通过函数指针将其和底层实现解耦,因此内核的同一套ethtool代码对于不同的net_device可以根据驱动实现相同的功能,而不用在乎底层不同设备的不同。 159 | 160 | 关于net_device我们在大致了解了它一下后就先告一段落,后面我们会再讲到它,在本章中它是我们的第一主角。目前我们只要知道,它代表了一个网络设备,其包含了这个网络设备的所有内核需要的通用方法的具体实现(通过函数指针)。 161 | 162 | 163 | ### 硬件中断&module机制 164 | 165 | 上面在讲net_device的时候讲到了它有一个属性irq代表了中断向量号,那么这里的中断是什么意思呢?先来看一个问题:如果我在键盘上敲下了一个键,那么我的内核是怎么知道键盘这个外部设备被敲下了这个键并做出响应的(比如在屏幕上显示这个键对应的字母)?在一般的事件驱动的软件开发中,我们一般都是以一个while循环来查看是否有新的事件产生,代码类似如下: 166 | 167 | ``` 168 | while True: 169 | event = get_event() 170 | if event.get("type") == "数字键1": 171 | print("1") 172 | elif event.get("type") == "数字键2": 173 | print("2") 174 | ...... 175 | sleep(1) 176 | ``` 177 | 178 | 对于这个事件驱动的模型存在一个问题,比如我print("1")的这个操作,如果其会花费30秒,那么我整个while循环就卡在这个print上了。在这30秒内我敲入的其它键内核可能就忽略掉了。另外可以看到这里一般会的有一个sleep操作,如果sleep期间产生了事件那么这个事件也得等sleep白白结束后才会被获取。因此这个模型确实管用,但其对事件的响应的及时性并不好,我们可以把它看成是一种轮询的模型。现在我们来看下内核的中断模型,并看下其和这个轮询模型的区别。 179 | 180 | 首先先说下,中断这个得CPU支持(其实内核设计成现在这样,很大一部分原因是CPU只能支持这样的设计,在这里是硬件决定软件,基础决定上层)。简单的说,我们的键盘连在了一块中断芯片上,然后这个中断芯片连在CPU上。当我们敲下一个键的时候,键盘产生一个电信号到中断芯片,中断芯片根据事先编程的规则,根据编程的逻辑对这个电信号做个转换,然后发送给CPU。CPU上接中断芯片的引脚对于CPU来说有特殊的含义:当这个引脚上产生电信号的时候,CPU会立刻中断并存储当前的执行环境,然后查询中断寄存器,根据引脚传过来的中断irq查看中断寄存器指向的中断向量表中偏移为irq的中断处理函数是什么,并立刻调用这个处理函数进行处理。然后内核就开始处理这个按键事件了,比如中断处理函数就是在屏幕上输出一些东西。在中断处理函数执行完成后内核会通过特定的汇编指令返回之前的上下文,因此会继续执行之前的内容。这就是一次最简单的中断事件的处理过程了。 181 | 182 | 当然实际情况会复杂一些,比如我们可以设置屏蔽中断,或者设置内核不响应中断等等。另外上面也提到过中断号共享,因为我们看到了中断需要中断芯片的支持,中断芯片支持的中断向量号是有限的,如果我们的设备数大于这个数目中断向量号就可能不够,此时多个设备可以共享一个中断向量号。 183 | 184 | 另外我们也看到了轮询模型,我们来比较下这两个模型。对于中断模型其存在一些缺点,打个比方当我网卡不停的收到大量数据包的时候,每个数据包都会触发一次中断,而我的内核还没处理完上一个数据包呢就被新的中断给打断了,这样就造成了一个恶性循环,内核永远无法处理完数据包,且大量的CPU时间都被响应中断时间给占用了。此时轮询模式就管用了,当内核感觉到数据包量特别大的时候内核可以通过轮询的方式去主动询问网卡而不是由网卡中断内核来获取数据包。这样只要内核没有处理完当前的数据包其就不会去问网卡要新的数据包,只有当前的数据包都处理完后,才会调用类似上面例子的get_event函数去从网卡一次性获取多个数据包然后进行处理。并且由于不处于中断的上下文,如果此时用于处理数据包所消耗的CPU时间过多,那么其可以主动让出CPU给其它进程使用。当然中断模型的优点也很明显,对于数据包量不大的情况,轮询模型的延迟会大于中断模型。轮询模型在我们下面讲收包的时候会讲到,内核中有NAPI支持对网卡的轮询。 185 | 186 | 除了最简单的中断和轮询外,还有一些别的方法,比如驱动发现数据包量很大的时候就不每个数据包都立刻产生一次中断了,其可能会等到连续有30个包后才一次性发个中断,这样可以减少中断的次数。另外也可以等待一定特定时间后才发送一个中断,也能一定程度上减少中断的次数。不过缺点是都会造成延迟。 187 | 188 | 有一个需要大家记在心里的东西:中断是和CPU的核绑定的。比如我的主机有32个核,那么可以简单的近似认为我有32个独立的中断芯片。对于设备我只能同一时间接在一个芯片上,换句话说如果我的这个设备接在了第一个核的芯片上,那么每次这个设备产生中断只有第一个核会响应,其它的核则感知不到这个事件。这也是为什么在以前没有网卡多队列或RPS的时候我们会发现CPU0的软中断时间特别高而其余核很空闲的原因,因为所有的网卡中断只有一个核能处理。 189 | 190 | 硬件中断我们就暂时讲这么多,接下来介绍下内核的module机制。这里需要说些内核或操作系统的历史了。 191 | 192 | 我们都知道操作系统是个很复杂的大程序,为了设计这么一个大程序,操作系统的设计者们将操作系统分成了很多很多的子系统,比如IO子系统,内存子系统,网络协议栈子系统等等。那么这些子系统如何组合在一起呢?有两种主流的方法。第一种是说我把我的操作系统内核设计的功能非常简单,只将非常核心非常必要的功能放在一起,其余的子系统我通过进程或线程的方式提供,当子系统之间要交互的时候可以通过如IPC之类的方法进行交互。第二种则是将所有的功能都编译在同一个操作系统可执行文件中,就是一个单独的进程。一般将前者称为微内核(也就是说内核的真正核心只提供核心且非常必要的少数功能),后者称为宏内核(也就是说所有的东西都塞在一起)。学术界貌似比较喜欢微内核,原因是这种架构的子系统替换或升级比较简单,只要停止原来的进程启动新的就行了(这样很便于他们研究),目前Windows的内核就是微内核的代表。但是对于linux则是采用的宏内核,所以在很久之前linux的设计者linus还和一些其他的知名内核设计者有过分歧,他坚持认为微内核进程间的通信会造成效率低下等问题,在他的坚持下linux目前是以宏内核的方式存在的。不过宏内核确实存在不足,比如我一个研究人员想试一下一个新的驱动,那我得重新编译一个完整的内核才行,所以linux在宏内核的基础下引入了module机制。关于宏内核和微内核的知识推荐大家可以看下操作系统的书籍。 193 | 194 | 现在来简单介绍下module机制,这个是我们之后会讲到的动态加载驱动程序的基础。module机制对于Python来说可以想象成我动态的通过imp模块加载一个python文件,然后加载入内存。之后就可以自动调用了。比如: 195 | 196 | ``` 197 | #加载模块,file_path类似于dog.py,module_name为dog 198 | modules = dict() 199 | mod = imp.load_source(module_name, file_path) 200 | modules[module_name] = mod 201 | 202 | ``` 203 | 204 | 然后我们的dog.py中有一个类,继承基类的run方法并覆盖之: 205 | 206 | ``` 207 | class Dog(Animal): 208 | 209 | def run(): 210 | print("Dog run run run.") 211 | ``` 212 | 213 | 此时当我的主程序想要调用时,直接调用如下代码即可: 214 | 215 | ``` 216 | modules.get("dog").run() 217 | ``` 218 | 219 | 那如果我想换个dog呢,很简单,我加载一个不同的dog文件,比如bad_dog.py,module_name依然为dog: 220 | 221 | ``` 222 | #这里file_path变为bad_dog.py 223 | mod = imp.load_source(module_name, file_path) 224 | modules[module_name] = mod 225 | ``` 226 | 227 | bad_dog.py代码如下: 228 | 229 | ``` 230 | class BadDog(Animal): 231 | 232 | def run(): 233 | print("Bad Dog run run run.") 234 | ``` 235 | 236 | 此时主程序再次调用时就会输出bad dog的run了。 237 | 238 | 内核中的module机制可以看成是类似的机制,只不过会更加复杂一些,并且需要用到编译、链接的一些知识及技巧,关于编译和链接,强烈推荐大家看下《程序员的自我修养----链接、编译与库》这本书,我们这里就不介绍了。我们可以将dog这个module_name看成是某个网卡的驱动,通过上面动态加载python文件的方式我们可以改变这个驱动的实际执行代码。下面是一个C语言版的真实的module代码,代码来自《Linux 设备驱动程序》: 239 | 240 | 241 | ``` 242 | #include 243 | #include 244 | MODULE_LICENSE("Dual BSD/GPL"); 245 | 246 | static int hello_init(void) 247 | { 248 | printk(KERN_ALERT "Hello, world\n"); 249 | } 250 | 251 | static void hello_exit(void) 252 | { 253 | printk(KERN_ALERT "Goodbye, cruel world\n"); 254 | } 255 | 256 | module_init(hello_init); 257 | module_exit(hello_exit); 258 | ``` 259 | 260 | 我们上面的例子中python文件加载就是直接加载,不会的调用什么方法,但是内核的module机制中提供了很多hook,比如这里的init和exit方法。我们完全可以在init中做足够的初始化工作,比如安装驱动,生成net_device设备等。当然如果要让我们上面的python代码页支持这种加载和卸载时的hook的话也很简单,读者可以自己仿照着改一改。 261 | 262 | 当我们写出这个文件后,编译它可以生成一个helloworld.ko的内核模块。然后我们可以通过insmod以及rmmod的方法加载、卸载这个模块。很容易可以猜到当我们insmod的时候内核会调用hello_init方法,而rmmod的时候内核会调用hello_exit方法,这一切全靠module_init和module_exit的帮助,它们提供了足够的符号信息让内核能正确定位模块中的init和exit方法。 263 | 264 | 举个简单的例子,比如我们的hello_init为eth0网卡设置了对应的net_device结构,并填充了各种ops函数,则内核在收发数据包的时候就会使用这里hello_init给eth0设置的ops函数。然后我们突然想换一个驱动了,于是我们调用rmmod卸载hello这个驱动,此时hello_exit就被调用,其做了清理工作,比如移除了net_device结构体。然后我们再用insmod加载一个别的驱动模块,此时我们的eth0就能在新的驱动程序下工作了。原理,就类似于我们的dog以及bad dog的例子,本质上是分层思想在起松耦合的作用。 265 | 266 | 关于硬件中断和module机制我们就讲这么多,大家只要记住外部设备如网卡会的通过发送中断信号的方式让内核感知到有事件发生了,然后内核就会通过中断号调用对应的中断处理函数。而module机制则是一个动态加载驱动或子系统的内核的一种机制,通过这种机制可以在不重新编译或重启内核的情况下实现驱动或子系统的更新。 267 | 268 | 269 | ### softirq 270 | 271 | 下面我们来讲softirq,softirq是中断的下半区的一种,所以我们先来讲中断的下半区。 272 | 273 | 上面我们讲过,外部设备通过硬件中断的方式让内核知晓有事件产生,然后内核会根据中断号调用对应的中断处理函数进行处理。我们将内核处理中断的这个过程称为中断上下文,中断上下文包括调用中断处理函数及处理函数的执行的过程。在中断上下文中,内核无法响应其它中断(或者说内核会的忽略其它中断)。从这里可以看出中断上下文必须极短才行,否则会的影响内核的正常工作。比如我们的进程间的切换就是通过定时器芯片定期的产生定时中断事件,内核得到这个中断事件后切换到对应的中断处理函数进行进程上下文的切换。想象一下如果一个中断处理函数需要30秒的时间,那么这30秒的时间内核不能响应任何其它中断,所以我们的进程也就不能切换,新收到的数据包也无法正常处理,这会严重影响我们的内核的正常工作。 274 | 275 | 那么如何解决这个问题呢?中断事件是必须响应的,因为这个是内核目前的最基本的获取外部事件的方式,所以中断上下文就一定会存在。能不能在中断上下文中响应其它中断呢?不大可行,最容易列出的缺点就是这样做会的极大的增加开发者的负担,开发者在开发中断处理函数的时候得时时挂念着自己的代码随时会被中断等等问题(本来内核就很复杂了)。所以内核提供了一种下半区的机制来解决这个问题。 276 | 277 | 下半区的思想很简单,既然中断上下文对时间的要求非常敏感,那么在中断上下文期间我就只把必须做的事情做了,然后剩下的耗时的工作我们就在内核的某个变量中记录下,记录内容类似:“内核,这里有点事,你有空的时候来继续完成”,然后我们的中断上下文就退出。等到某个合适的时机,比如内核开始空闲的时候,内核就会的去查看是不是有要做的事情,如果发现有要做的事情那么内核就会去处理这些事情,注意此时内核在处理这些事情的时候内核是可以响应其它中断的。那么在哪些时机内核会的去检查是不是有要做的事情呢?一种时机是内核进行进程间切换的时候(也就是在schedule函数执行的时候),内核会做这样的检查。 278 | 279 | 下半区的概念我们清楚后,我们来说下内核的支持下半区的具体实现(下半区只是个思想,这里将的是支持这种思想的实现)。目前内核提供了多种实现,softirq是其中的一种,由于我们的协议栈的实现主要是通过softirq来做下半区的处理的,所以我们这里只讲softirq。 280 | 281 | 网上很多资料讲softirq翻译成软件中断或软中断,由于这个翻译会的和int 80这种程序发起的中断造成混淆,所以这里小秦还是将它称为softirq。 282 | 283 | 目前softirq支持处理下面这些类型的下半区工作: 284 | 285 | ``` enum { HI_SOFTIRQ=0, TIMER_SOFTIRQ, NET_TX_SOFTIRQ, NET_RX_SOFTIRQ, SCSI_SOFTIRQ, TASKLET_SOFTIRQ }; 286 | ``` 287 | 288 | 这里NET_TX_SOFTIRQ是处理发包的下半区,而NET_RX_SOFTIRQ是处理收包的下半区。举个例子,当一个数据包到达的时候,网卡触发了某个中断通知内核,内核在收到这个中断后,调用收包的中断处理函数,进入中断上下文,中断上下文在我们的例子里做了一个最简单的操作,就是将数据包从网卡拷贝到内核的sk_buff结构体中,然后内核做了个标记说我这里收到了个包(简单的说就是将某个全局变量的NET_RX_SOFTIRQ这一位标记为1),等有空的时候记得回来处理这个包。然后中断上下文就退出了。之后在某个时机比如上面说的进程切换的时候,内核会检查是否有标记说有数据包等着被处理,此时内核发现了之前留下的标记,于是调用某个函数开始处理这个数据包。 289 | 290 | 内核检查标记位的时候是按照我们上面列出来的顺序来检查的,也就是说如果TIMER_SOFTIRQ和NET_TX_SOFTIRQ都被标记了,那么内核会先处理TIMER_SOFTIRQ然后再处理NET_TX_SOFTIRQ。换句话说就是这里的顺序决定了下半区中等着被处理的任务的处理优先级。 291 | 292 | 那么内核怎么知道应该调用哪个函数来处理我们上面例子中的NET_RX_SOFTIRQ呢?通过open_softirq这个函数。比如open_softirq(TASKLET_SOFTIRQ, tasklet_action, NULL)就能将TASKLET_SOFTIRQ和tasklet_action这个处理函数绑定起来。 293 | 294 | 另外内核保证某个一核上只能执行一个softirq。简单的说就是在执行softirq的时候内核会通过加锁的方式保证不会同时运行多个softirq,比如第一个下半区处理函数在执行的时候被中断打断了,并且这个中断是用来切换进程的,于是又有机会调用softirq的入口函数,但是此时这次调用就会失败了,因为之前已经在运行softirq了。 295 | 296 | 我们再来继续看下内核调用收包的下半区的处理函数后,softirq会有什么行为。我们上面说过在最简单的情况下,在中断上下文的时候内核会将数据从网卡拷贝出来放到sk_buff中,那么这个sk_buff肯定是要放到某个链表或类似数据结构中的,这个链表的头被谁拥有呢?链表的头在内核中由softnet_data拥有,系统启动的时候内核会对每个CPU核初始化一个softnet_data结构体,所以如果某个收包的中断是在CPU0上接收的,那么这个数据包会放在softnet_data结构体中的input_pkt_queue链表中。然后在下半区处理函数执行的时候,其就会去这个链表上取数据包(也就是取sk_buff),然后调用某个函数让这个数据包走协议栈。一切看起来都很简单,但实际情况却会复杂一些。我们上面说过在softirq执行期间,内核是可以响应中断的,所以可能softirq在执行完这个数据包后其再次检查我们的标记的时候又发现有要被处理的数据包了,于是其又要开始走这个流程。可能有人会想这个没关系,其它进程需要时间的时候反正你softirq是可以被中断的,那么哪怕你这里是个死循环我其它进程中断了其它进程也能正常执行,但问题是切换的时候我们的schedule会调用softirq,换句话说只要我们的标记位一直存在那么我们的softirq的处理函数会一直占有我们CPU时间,而不给其它进程执行的机会。所以最简单的方法就是让softirq在单次执行的时候其检查标记位的次数有个上限,当超过这个上限的时候就让出CPU时间,然后其余进程就能执行了。但当中断次数非常高的时候还是会存在问题,比如我下次切换进程的时候又得进入softirq的处理流程了,然后又是要等到其执行到上限后才能切换到下个进程,这会严重影响系统的响应时间。所以内核提供了ksoftirqd这个内核线程来解决这个问题。 297 | 298 | ksoftirqd的处理逻辑很简单,其也是调用了softirq的入口函数,这个函数和我们上面举例中说的schedule中的一样,所以其会执行通常的softirq处理逻辑。当出现我们上面说的中断过多造成其它进程得不到CPU时间片的时候,softirq的处理函数会将任务交给ksoftirqd内核线程来完成。由于我们上面讲了内核通过锁等方法保证一个核只能运行一个softirq处理函数,所以当ksoftirqd开始运行处理函数的时候,在类似schedule等函数去调用softirq处理函数的时候其会发现有别的线程在处理了,所以这里就不会进入softirq的处理逻辑,这样也就能保证下一个进程可以及时被调度。另外ksoftirqd的内核线程优先级很低,在系统繁忙的时候内核就能保证其它进程的优先工作,只有在内核空闲的时候才开始调用ksoftirqd(但这并不是因为这ksoftirqd不重要,如果我一个数据包来不及接收,其它进程也相应的会从运行状态转为等待,让内核空闲出来,此时ksoftirqd就能运行了)。所以如果我们的主机收发数据包量很大的时候,我们经常能通过top等命令看到ksoftirqd占用了大量的CPU时间,同时也能观察到top命令的si这一项很高。一般对于这种情况的解决方法是看一下si或中断次数是不是集中在单核,如果是单核的话看下网卡支不支持多队列,如果支持则尝试绑定网卡队列和核,让它们不集中在一个核,如果不支持则查看内核是否支持RPS,RPS可以看成是软件实现的多队列。如果此时CPU使用率还是非常高(并且通过sar等命令确定是网络引起的),那么要判定下是不是正常的流量,否则可能就是被攻击了,对于攻击则就要采用攻击对应的应急方案,或者通过自动化的处理脚本自动修复。 299 | 300 | 对于softirq我们就讲这么多,需要大家记住的其实就两点:内核将耗时但不是必要的工作在下半区中执行而不是在中断上下文中执行,另外每个CPU核拥有一个softnet_data结构体保存每个CPU的独有信息,而最基本的收包方式中,我们的待处理sk_buff就绑在该结构下面。 301 | 302 | 303 | ### proc文件系统 304 | 305 | proc文件系统(以及/proc/sys)是一个我们可以通过普通操作文件的方法(cat/echo)来获取或改变内核变量的一个从用户态和内核态进行交互的机制。我们在之前提过,内核通过module机制实现了一个分层的松耦合的架构,proc可以看成是虚拟文件系统这一层的一个非常有用的例子。首先先说下什么是文件系统,我们都知道硬盘中可以存放我们的文件,当我们要访问这些文件的时候我们需要一个机制告诉我们我们的文件bingotree.txt存放在硬盘的哪个地方。文件系统最简单的功能就是存放了我们逻辑上看到的物理文件以及其真实的在磁盘上存储的扇区信息的映射关系的一个东西。最简单的,其提供了read和write两个函数,当我们调用如read("bingotree.txt")的时候,文件系统就会根据文件名(或者更贴切的说文件路径)找到bingotree.txt这个文件对应的扇区信息,然后通过IO子系统读取硬盘上的信息到内存,然后read函数会返回一个指向这块内存的指针。write函数则类似,只不过这里是通过IO子系统写入对应的内容到磁盘上。 306 | 307 | 从上面的表述可以看出,我们的文件系统只需要提供类似于read或write等少数的基础方法即可。对于我们这么一个支持module机制的内核,我们完全可以把文件系统这一块做个分层,提供一个基类,基类拥有read和write方法,但是具体的实现则由加载的模块实现。所以我们会看到在linux中存在很多不同格式的文件系统,比如ext3、ext4等。在linux的世界,这种文件系统的松耦合的实现方式被叫做虚拟文件系统。 308 | 309 | 既然虚拟文件系统这么灵活,所以内核实现了proc这么一个文件系统(也就是一个proc的module),它的read和write等方法有点特别,当我调用read("/proc/uptime")的时候,proc的module的read方法并不会将/proc/uptime翻译成磁盘上的扇区信息,而是会去读取内核中的uptime信息获取系统启动了多少时间。通过这种方法,我们可以通过cat或echo这种命令获取或改变内核中的变量。 310 | 311 | 当然上面说的比较简单,但是其功能基本上就是这样,有兴趣的同学可以深入学习下。对于我们这本基础的书籍来说,大家只要知道虚拟文件系统是啥,以及proc文件系统的读写本质上是转换到内核变量的读写即可。 312 | 313 | 314 | ### 总结 315 | 316 | 本节主要是为本章下面的小节做准备工作。希望大家能记住的是: 317 | 318 | * sk_buff代表一个数据包 319 | * net_device代表一个网络设备 320 | * 硬件中断是外部设备告知事件发生的一种机制,大家需要知道中断向量号及处理函数有什么用 321 | * softirq的简单原理,ksoftirqd内核线程的作用以及softnet_data是每个核独立拥有的 322 | * 虚拟文件系统以及proc文件系统的作用 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | -------------------------------------------------------------------------------- /book/ch05/s5.2.md: -------------------------------------------------------------------------------- 1 | ## 协议栈中的收包/发包 2 | 3 | 本节我们会讲一下在内核中,一个数据包从网卡进入后会走什么样的路径,以及当发送一个数据包的时候这个数据包会的走什么样的路径。除此以外当我们对数据包的收发路径大概清楚后,我们会看下Neutron中用到的veth设备在内核是如何实现的,另外会讲下常见的提升收发包速率的方法。 4 | 5 | 6 | ### PCIe设备驱动的加载 7 | 8 | 在前面一节中我们提到了内核的module机制以及硬件中断的处理函数,另外也介绍了net_device数据结构。这里我们来把这几个东西串起来。 9 | 10 | 当一台物理机上插了块网卡的时候,网卡现在都是走PCIe口插在主板上的。假设现在内核中没有任何的网卡驱动,所以当开机的时候内核会让PCIe子系统自己去发现连在上面的设备,于是PCIe子系统发现了这个网卡。PCIe是个通用的标准,虽然此时PCIe不知道如何去使用这个设备,但是通过PCIe的标准PCIe子系统可以询问这个网卡设备获取这个网卡设备的基本信息,在这个信息里有一个很重要的ID,这个ID是全球唯一的,只属于这个网卡的型号。然后PCIe虽然不知道怎么使用这个设备,但是既然知道了其ID,所以就在某个内核中存了个信息,内容类似为:“我这里有个ID位XXX的设备。”。接着我们来加载驱动,加载的方法就是之前讲的module机制,通过insmod的方法我们的驱动就加载了,然后对应的init方法会调用。由于这个驱动知道自己的网卡是个PCIe设备,所以其init方法里会直接调用内核提供的pci_module_init函数。pci_module_init函数是内核提供的,其实现在目前我们可以简单的理解为:它会去查看PCIe发现的设备列表,遍历所有设备,然后判断被遍历的设备的ID是不是和驱动代码里写的ID一样,如果这两个ID是一样的,那么这个设备就有对应的驱动了。一个实际的例子如下: 11 | 12 | ``` 13 | static int __init e100_init_module(void) 14 | { 15 | return pci_module_init(&e100_driver); 16 | } 17 | 18 | static struct pci_driver e100_driver = { 19 | .name = DRV_NAME, 20 | .id_table = e100_id_table, 21 | .probe = e100_probe, 22 | .remove = __devexit_p(e100_remove), 23 | #ifdef CONFIG_PM 24 | .suspend = e100_suspend, 25 | .resume = e100_resume, 26 | #endif 27 | }; 28 | ``` 29 | 30 | 我们的module的init方法为e100_init_module,其直接调用了pci_module_init去匹配对应的设备。e100_driver是我们驱动实现的,注意这里的id_table属性就是我们的PCIe的每个设备型号全球唯一的ID。 31 | 32 | 现在我们知道了设备和驱动是如何联系起来的,接着我们来看我们的net_device如何被建立。这里还是要回到pci_module_init上,我们可以看到我们的驱动实现了一个pci_driver结构体,对于每个PCIe的驱动,都需要实现这个结构体的方法。内核约定在pci_module_init被调用的时候,会调用pci_driver结构体的probe方法,我们可以将probe看成是我们驱动和设备关联后执行的一个初始化方法。至于probe中做什么事情就有PCIe的驱动自己实现了。对于网卡驱动来说一般会做下面的事情: 33 | 34 | * 生成net_device结构体并注册到全局链表上 35 | * 驱动会设置net_device的相关属性,尤其是提供各种ops属性的实现 36 | 37 | 我们来详细看下probe,这里来看一个实际的例子e1000,在内核源码中的路径在drivers/net/ethernet/intel/e1000下。 38 | 39 | 首先来看其pci_driver结构体: 40 | 41 | ``` 42 | static struct pci_driver e1000_driver = { 43 | .name = e1000_driver_name, 44 | .id_table = e1000_pci_tbl, 45 | .probe = e1000_probe, 46 | .remove = e1000_remove, 47 | #ifdef CONFIG_PM 48 | /* Power Management Hooks */ 49 | .suspend = e1000_suspend, 50 | .resume = e1000_resume, 51 | #endif 52 | .shutdown = e1000_shutdown, 53 | .err_handler = &e1000_err_handler 54 | }; 55 | ``` 56 | 57 | 对应的probe方法我们看到是e1000_probe,我们来看下其实现。代码为: 58 | 59 | ``` 60 | static int e1000_probe(struct pci_dev *pdev, const struct pci_device_id *ent) 61 | { 62 | struct net_device *netdev; 63 | struct e1000_adapter *adapter; 64 | struct e1000_hw *hw; 65 | 66 | static int cards_found = 0; 67 | static int global_quad_port_a = 0; /* global ksp3 port a indication */ 68 | int i, err, pci_using_dac; 69 | u16 eeprom_data = 0; 70 | u16 tmp = 0; 71 | u16 eeprom_apme_mask = E1000_EEPROM_APME; 72 | int bars, need_ioport; 73 | ``` 74 | 75 | 这里声明及初始化了一些变量,接着的代码为: 76 | 77 | ``` 78 | /* do not allocate ioport bars when not needed */ 79 | need_ioport = e1000_is_need_ioport(pdev); 80 | if (need_ioport) { 81 | bars = pci_select_bars(pdev, IORESOURCE_MEM | IORESOURCE_IO); 82 | err = pci_enable_device(pdev); 83 | } else { 84 | bars = pci_select_bars(pdev, IORESOURCE_MEM); 85 | err = pci_enable_device_mem(pdev); 86 | } 87 | if (err) 88 | return err; 89 | 90 | err = pci_request_selected_regions(pdev, bars, e1000_driver_name); 91 | if (err) 92 | goto err_pci_reg; 93 | 94 | pci_set_master(pdev); 95 | err = pci_save_state(pdev); 96 | if (err) 97 | goto err_alloc_etherdev; 98 | ``` 99 | 100 | 这些代码和cpi有关,但是不是我们的重点。我们继续看代码: 101 | 102 | ``` 103 | err = -ENOMEM; 104 | netdev = alloc_etherdev(sizeof(struct e1000_adapter)); 105 | if (!netdev) 106 | goto err_alloc_etherdev; 107 | ``` 108 | 109 | 可以看到这里调用了内核提供的alloc_etherdev分配了一个以太网的net_device结构体。alloc_etherdev实际上是alloc_netdev的一个封装,alloc_netdev会分配已个最基本的net_device结构,然后alloc_etherdev在调用alloc_netdev的基础上会对这个net_device设置一些以太网相关的属性,这些以太网相关属性的初始化是通过调用ether_setup实现的。另外我们可以看到alloc_etherdev传递了一个e1000_adapter结构体的大小,这个大小是分配net_device的时候用于决定私有数据大小的。现在我们的net_device已经出现,只不过是一个通用的以太网的net_device结构体。我们继续看代码: 110 | 111 | ``` 112 | SET_NETDEV_DEV(netdev, &pdev->dev); 113 | 114 | pci_set_drvdata(pdev, netdev); 115 | adapter = netdev_priv(netdev); 116 | adapter->netdev = netdev; 117 | adapter->pdev = pdev; 118 | adapter->msg_enable = netif_msg_init(debug, DEFAULT_MSG_ENABLE); 119 | adapter->bars = bars; 120 | adapter->need_ioport = need_ioport; 121 | 122 | hw = &adapter->hw; 123 | hw->back = adapter; 124 | ``` 125 | 126 | 这里的代码主要是私有数据结构的初始化,私有数据结构所在的内存大小就是上面说的alloc_etherdev传递e1000_adapter结构体的大小。私有的我们就不去细看了,大家可以注意下netdev_priv,在大家自己去看网络相关的代码的时候看到netdev_priv就知道返回的指针是指向net_device的私有数据内存的指针。继续看代码,我们跳过那些私有的代码: 127 | 128 | ``` 129 | netdev->netdev_ops = &e1000_netdev_ops; 130 | e1000_set_ethtool_ops(netdev); 131 | netdev->watchdog_timeo = 5 * HZ; 132 | netif_napi_add(netdev, &adapter->napi, e1000_clean, 64); 133 | 134 | strncpy(netdev->name, pci_name(pdev), sizeof(netdev->name) - 1); 135 | ``` 136 | 137 | 我们可以看到我们的netdev_ops和ethtool_ops都在这里被驱动给设置了。关于napi相关的我们后面会详细讲到。e1000_netdev_ops的内容为: 138 | 139 | ``` 140 | static const struct net_device_ops e1000_netdev_ops = { 141 | .ndo_open = e1000_open, 142 | .ndo_stop = e1000_close, 143 | .ndo_start_xmit = e1000_xmit_frame, 144 | .ndo_get_stats = e1000_get_stats, 145 | .ndo_set_rx_mode = e1000_set_rx_mode, 146 | .ndo_set_mac_address = e1000_set_mac, 147 | .ndo_tx_timeout = e1000_tx_timeout, 148 | .ndo_change_mtu = e1000_change_mtu, 149 | .ndo_do_ioctl = e1000_ioctl, 150 | .ndo_validate_addr = eth_validate_addr, 151 | .ndo_vlan_rx_add_vid = e1000_vlan_rx_add_vid, 152 | .ndo_vlan_rx_kill_vid = e1000_vlan_rx_kill_vid, 153 | #ifdef CONFIG_NET_POLL_CONTROLLER 154 | .ndo_poll_controller = e1000_netpoll, 155 | #endif 156 | .ndo_fix_features = e1000_fix_features, 157 | .ndo_set_features = e1000_set_features, 158 | }; 159 | ``` 160 | 161 | 这里的一些方法(比如ndo_start_xmit)会在后面的小节中讲到。接着看我们的probe方法可以看到: 162 | 163 | ``` 164 | strcpy(netdev->name, "eth%d"); 165 | err = register_netdev(netdev); 166 | if (err) 167 | goto err_register; 168 | ``` 169 | 170 | 这里可以看到我们的设备名字被设置了,重点可以看到register_netdev,其将我们的net_device结构体加入了全局的链表中,此时我们调用ip link show的时候后者就会遍历全局的链表输出我们的设备了。 171 | 172 | probe的代码在e1000中就基本上这些,这里可能有人会有疑问,我们的net_device已经建立好了,但是我们的中断处理函数和中断向量号的绑定是在哪里设置的呢?目前代码看下来我们的probe并没有做这个事情,此时如果网卡收到了包产生了中断号,但由于这个中断号并没有对应的处理函数所以内核是收不到包的。在e1000的代码中,中断向量号和中断处理函数的绑定是在netdev_ops的ndo_open方法中设置的,在e1000_open中我们可以看到如下代码: 173 | 174 | ``` 175 | static int e1000_request_irq(struct e1000_adapter *adapter) 176 | { 177 | struct net_device *netdev = adapter->netdev; 178 | irq_handler_t handler = e1000_intr; 179 | int irq_flags = IRQF_SHARED; 180 | int err; 181 | 182 | err = request_irq(adapter->pdev->irq, handler, irq_flags, netdev->name, 183 | netdev); 184 | if (err) { 185 | e_err(probe, "Unable to allocate interrupt Error: %d\n", err); 186 | } 187 | 188 | return err; 189 | } 190 | ``` 191 | 192 | request_irq这里设置了adapter->pdev->irq这个中断向量号和中断handler的关系。另外通过上面的代码我们也清楚了,当我们想要学习一个驱动的收包代码的时候,e1000_intr会是我们的一个很好的入口:)。 193 | 194 | 关于PCIe的网卡驱动加载我们就讲这么多,重要的是要记住net_device是如何生成的以及其在哪里进行了ops的函数指针的赋值。 195 | 196 | 197 | ### 收包路径 198 | 199 | 现在我们来讲收包。在本节和本书下面的章节中我们会看到数据包如何从网卡进入内核,然后开始走协议栈一层一层往上走。我们不会讲高层协议栈中对于收包的处理,因为对于我们学习Neutron来说那个是应用的事情了,但是我想如果能理解下面讲的基础收包路径,那么大家很容易能继续探索协议栈上层收包的实现的。本节主要讲一个数据包是如何从网卡到内核并开始走协议栈的这么一个流程。 200 | 201 | 在前面的小节中我们已经看到了net_device这个结构体的初始化,下面我们就以eth0这个网卡为例子来看下其收包的逻辑。eth0的网卡插在PCIe卡上,然后根据上节说的内容,根据eth0所在网卡型号的全球唯一ID,驱动程序会认领这个设备,然后初始化net_device的属性。另外在我们上面e1000的例子中,当网卡open的时候其会绑定中断向量号和对应的中断处理函数e1000_intr。 202 | 203 | 内核中收包的数据路径目前有两种主流的,一个是基本的收包路径,简单的说就是一个包到达后先把这个包放到一个队列里,然后下半区的softirq handler会来处理,另一个是NAPI(New API,这名字有点俗...),后者可以通过poll这个轮询的方法处理数据包。这两种路径小秦下面都会讲到。 204 | 205 | 首先我们来讲NAPI,为什么现讲NAPI而不先讲基础的收包流程呢?因为目前内核中基础的收包流程是基于NAPI的流程实现的。 206 | 207 | 我们讲PCIe设备驱动加载的时候说到过probe方法会的进行net_device的初始化,net_device有一个间接关联的重要属性叫做poll,其也是一个函数指针。这个poll的实际实现由驱动实现,其作用是说内核可以通过调用poll直接从网卡上获取一定的数据包。这里可以看出poll其实是类似轮询的方法,由内核主动去向网卡获取数据包而不是由网卡通过中断汇报数据包给内核。 208 | 209 | 现在想象下一个数据包到达了网卡,然后网卡会触发硬件中断,硬件中断打断了CPU的当前执行上下文转到了内核中处理中断相关的代码,然后这段内核代码根据中断号找到中断处理函数,接着调用这个函数。这个中断处理函数根据我们上面小节所讲的是由驱动提供的。对于NAPI的驱动来说,这里中断处理函数做的事情主要有两个:第一是将当前的net_device结构体加入到我们在5.1小节将的每个CPU核独有的softnet_data结构体的poll_list链表上,第二是设置下收包的softirq标记NET_RX_SOFTIRQ。然后中断处理函数就结束了(可以看到这个中断函数没有做太多是事情)。 210 | 211 | 接着softirq上场了,我们已经知道softirq和硬件中断类似,都会根据某个标识去查找对应的处理函数。内核中NET_RX_SOFTIRQ的处理函数为net_rx_action。 212 | 213 | net_rx_action的逻辑是这样的,其会遍历softnet_data的poll_list,对于poll_list上的每个net_device设备调用其poll函数从网卡上获取数据包。拿到数据包后就开始走协议栈了,怎么样才能让一个数据包走内核的协议栈呢?poll函数中可以调用内核提供的netif_receive_skb,并传递sk_buff,netif_receive_skb会的让这个数据包走协议栈。 214 | 215 | 上面已经看到了NAPI是怎么让数据包从网卡进入到走协议栈的一个简单流程,当然有很多细节目前是略过的,比如对于poll的调用每次是只能获取一定数量的数据包的,否则一个网卡上如果不停的有数据包进来那么整个系统会卡在这个网卡上。所以可以近似的认为每次只能获取n个数据包,如果获取完n个后还有数据包可以获取,那么会先将这个net_device移动到poll_list的末尾,然后开始处理下一个poll_list上的net_device。还有就是NAPI其实是在系统压力较大的时候才会开始poll,平时都是正常中断响应数据包的。不过基本的流程就是上面讲的这样。 216 | 217 | 现在来看下非NAPI的收包流程,也就是基本的收包流程。想象一下一个数据包到达了网卡,然后网卡会触发硬件中断,接着中断处理函数被调用。在非NAPI的驱动程序中,内核规范了下面的执行流程:驱动负责从网卡拷贝一个数据包到内核的一个sk_buff中,然后中断调用内核提供的netif_rx方法让这个数据包开始走收包的流程。在以前没有NAPI的时候netif_rx做的事情就是把这个sk_buff放到softnet_data的nput_pkt_queue链表中,然后设置softirq标记等待处理函数来处理。但是现在的内核的netif_rx的逻辑已经改了,现在的netif_rx会的建立一个此网卡net_device结构体对应的类似的net_device结构体(比如名字叫做net_device_2),然后netif_rx会把这个net_device_2放到softnet_data的poll_list上,同时和以前的实现一样,会从网卡获取数据包到sk_buff并将其放到softnet_data的nput_pkt_queue链表中,然后设置softnetirq标记等待处理函数来处理。通过上面的NAPI的流程学习我们知道这里的处理函数net_rx_action会遍历poll_list,然后一次调用遍历得到的net_device的poll方法。于是我们的net_device_2的poll方法被调用。此时这个poll方法就是由内核实现的了,一般是绑定到内核提供的process_backlog函数上。这个函数做的事情就是从softnet_data的nput_pkt_queue链表中获取一个sk_buff,然后调用上面讲到的netif_receive_skb让这个数据包开始走协议栈。可以看到通过修改netif_rx的实现对于老的驱动代码不需要改动就能共用同一套NAPI的基础架构。 218 | 219 | 那么NAPI有什么好处呢?对于Neutron网络节点来说,在非DVR这种情况下列如外网口的这类网卡,其数据包的个数经常是很大的,数据包量大了后中断的方式效率就太低下了,所以通过NAPI的poll方法可以减少网卡的负担,由CPU主动根据情况去网卡上获取数据包。另外NAPI其实真实的实现是会在中断和轮询这两者间切换的,比如一个新的数据包到达,然后发现内核很繁忙且还有未处理的数据包的时候,其会将中断模式改为poll,于是新的数据包到达后就不会打断内核,同时当内核空闲的时候其会去轮询设备,查看是否有要处理的数据包。当压力下来后又可以恢复中断模式。通过这种方法在有大量的数据包环境中可以减少CPU使用率,并在只有少量数据包的时候降低响应时间。 220 | 221 | 另外这里希望大家能想一下从网卡收到包到这个包触发硬件中断以及等待softirq的执行直到最终准备走协议栈,这一切是在哪个CPU核上发生的?这些对性能是否有影响? 222 | 223 | 现在我们的数据包sk_buff已经站在内核协议栈的门口了,通过netif_receive_skb其会开始内核协议栈之旅,对于netif_receive_skb在我们后面的章节中会讲到其实现。 224 | 225 | 226 | 227 | ### 发包路径 228 | 229 | 我们来看发包路径。应用层的某个应用调用socket发包后,数据包会组织成sk_buff结构体,然后经过协议栈一直往下直到协议栈的最下层开始准备发包。此时内核会调用net_device的某个函数将sk_buff发送出去。我们来看下这个流程。 230 | 231 | 在看流程之前我们要先看下TC(Traffic Control)。在本书的前面章节已经介绍了tc命令的用法以及qdisc的概念,这里再来复习一下。内核中的发包可以简化的看成这么一个过程:内核将要发送的sk_buff放到一个队列中,然后内核再从这个队列中取出一个数据包,然后再调用net_device的驱动实现的发包函数将数据包传到网卡上让网卡把包发送出去,伪代码如下: 232 | 233 | ``` 234 | queue.put(sk_buff) 235 | sk_buff = queue.get() 236 | net_device.hardware_send_packet(sk_buff) 237 | ``` 238 | 239 | 可能有人要问了:为什么不直接调用net_device.hardware_send_packet将sk_buff发送出去而要走一下队列呢?原因在于我们要做TC,也就是要做流量的整形,限速就是流量整形的一种,比如我希望控制发包速率在每秒100个,否则我的交换机等物理设备承受不了。上层应用当然不会去主动做这个限速啦,所以这个事情得内核在做。怎么做呢?就是通过这个队列。比如我要限制每秒100个包,那么内核只要实现一个队列,这个队列的queue.get()方法每10ms才会返回一个数据包(即便很多数据包都积压在队列中了),然后在不考虑hardware_send_packet等函数的开销的时候我们就将发包的速率控制在了100pps了。 240 | 241 | 这里的queue在net_device中就是qdisc属性。我们可以用tc配置各种规则决定如何对流量进行整形(这就是tc的实现了,有兴趣的同学可以学习下)。所以我们可以看到内核在发包的时候是可以做限速的。在我们Neutron提供服务的时候,QoS是很重要的一块,如果大家希望实现限速那么可以考虑使用tc。 242 | 243 | 现在来看下完整的流程,上面的流程没有考虑太多的异常情况,其中最重要的一种异常是网卡繁忙。什么是网卡繁忙呢?我们知道系统中一般只有两到四块网卡,但是却往往有30几个CPU核。假设同一时间这30几个核都调用了发包流程,那么我们的网卡是不能同时响应的。在不考虑多队列等情况下,如果我有4块网卡,那么同一时间只能同时满足4个核的需求,此时其它核的发包请求就失败了。也就是我们上面代码中的net_device.hardware_send_packet(sk_buff)会返回错误。此时总不能把这个数据包丢掉吧,这样重传的代价太大了,那么咋办呢?此时我们的发包的softirq就开始起作用了。 244 | 245 | 当net_device.hardware_send_packet(sk_buff)无法正常工作的时候(其实内核通过锁机制并不会真的去调用该方法),内核会执行netif_schedule这个内核提供的函数将这个net_device放到每个CPU核的softnet_data的output_queue中,然后标记下NET_TX_SOFTIRQ这个标记位,接着会将这个没有发送出去的sk_buff重新调用上面伪代码中说的queue.put(sk_buff)放回到qdisc的队列中。然后此次发送就认为是成功了,内核会返回到上层调用代码出。 246 | 247 | 接着softirq开始执行,NET_TX_SOFTIRQ对应的处理函数会遍历每个CPU核的softnet_data的output_queue,对上面的net_device调用sk_buff = queue.get()以及net_device.hardware_send_packet(sk_buff)这个流程,如果发送成功那就太好了,如果还是失败的话就继续走上面的流程,把net_device继续留在ouput_queue中,把sk_buff继续放回到队列里,然后再标记下NET_TX_SOFTIRQ等着下一次继续处理。 248 | 249 | 对于qdisc来说,其队列提供的方法比我们伪代码提供的要复杂一些。主要有三个: 250 | 1. enqueue:类似我们put,第一次放一个sk_buff到qdisc中 251 | 2. dequeue:类似我们的get,从qdisc重取出一个sk_buff 252 | 3. requeue:类似我们发送失败后执行的put,将sk_buff再次放回qdisc 253 | 254 | 大家如果深入的去学习过tc的话,对于这三个方法肯定印象会比较深刻,记得小秦一开始学tc的时候觉得有些概念特别难以理解,现在知道了发包时候走tc的流程后就觉得简单了许多。 255 | 256 | 另外要提一下就是不是所有的设备都会的走qdisc这一套流程,有可能直接调用发包函数发包出去了。比如对于lo这种设备就没有qdisc的相关操作。这一点我们从下面的代码里可以看到: 257 | 258 | ``` 259 | ...... 260 | if (q->enqueue) { 261 | rc = __dev_xmit_skb(skb, q, dev, txq); 262 | goto out; 263 | } 264 | 265 | /* The device has no queue. Common case for software devices: 266 | loopback, all the sorts of tunnels... 267 | 268 | Really, it is unlikely that netif_tx_lock protection is necessary 269 | here. (f.e. loopback and IP tunnels are clean ignoring statistics 270 | counters.) 271 | However, it is possible, that they rely on protection 272 | made by us here. 273 | 274 | Check this and shot the lock. It is not prone from deadlocks. 275 | Either shot noqueue qdisc, it is even simpler 8) 276 | */ 277 | ...... 278 | static const struct net_device_ops loopback_ops = { 279 | .ndo_init = loopback_dev_init, 280 | .ndo_start_xmit= loopback_xmit, 281 | .ndo_get_stats64 = loopback_get_stats64, 282 | .ndo_set_mac_address = eth_mac_addr, 283 | }; 284 | ``` 285 | 286 | 这里还是要提个问题给大家,当我应用调用socket发包的时候,这个数据包的发送过程是都在一个CPU核上发生的吗? 287 | 288 | 现在我们对于一个数据包怎么从协议栈的底部到出网卡已经大致清楚了。如果大家平时发现NET_TX_SOFTIRQ这个softirq执行的频率很高(此时根据我们之前说的ksoftirqd的CPU占用率也会很高),那么可能是网卡过于繁忙造成的。 289 | 290 | 291 | ### lo/veth的实现 292 | 293 | 294 | 这里我们来看两个虚拟设备的驱动收发包过程。我们上面讲的例子都是以物理网卡来讲的,但实际上在内核中物理网卡和虚拟网卡都是通过net_device来保存期相关统计信息、操作函数等信息的。我们会分析两个设备的实现,一个是loopback设备,另一个是veth。lo大家肯定都不陌生,veth的话我们在Neutron中已经看到过了,其会用于连接br-int以及我们的qbr。 295 | 296 | 首先先来看lo的实现。我们看下一个数据包从lo口发送的时候会发生什么吧。根据我们前一节讲的,当一个包被发送的时候,会的准备走net_device的qdisc队列进行流量的整形。但是我们也讲了如果net_device的qdisc的enqueue方法为空的话则不会走这段逻辑。我们的loopback设备的net_device就没有这个enqueue方法,所以其会的直接调用net_device上的发送函数(也就是ndo_start_xmit这个函数指针)。对于lo设备来说这个函数的实现为loopback_xmit。我们看下其实现: 297 | 298 | ``` 299 | static netdev_tx_t loopback_xmit(struct sk_buff *skb, 300 | struct net_device *dev) 301 | { 302 | struct pcpu_lstats *lb_stats; 303 | int len; 304 | 305 | skb_orphan(skb); 306 | 307 | /* Before queueing this packet to netif_rx(), 308 | * make sure dst is refcounted. 309 | */ 310 | skb_dst_force(skb); 311 | 312 | skb->protocol = eth_type_trans(skb, dev); 313 | 314 | /* it's OK to use per_cpu_ptr() because BHs are off */ 315 | lb_stats = this_cpu_ptr(dev->lstats); 316 | 317 | len = skb->len; 318 | if (likely(netif_rx(skb) == NET_RX_SUCCESS)) { 319 | u64_stats_update_begin(&lb_stats->syncp); 320 | lb_stats->bytes += len; 321 | lb_stats->packets++; 322 | u64_stats_update_end(&lb_stats->syncp); 323 | } 324 | 325 | return NETDEV_TX_OK; 326 | } 327 | ``` 328 | 329 | 我们上面说过,物理设备的发包在这里应该是调用驱动相关的代码发送数据包出去,但是在loopback_xmit中我们看到其并没有真实的发送数据包出去,而是直接调用了netif_rx(还记的netif_rx不?不记得的话可以翻翻上面收包的小节哈)。netif_rx会把我们的数据包sk_buff放到cpu核的softnet_data的input_queue上,然后就开始走正常的收包流程了。 330 | 331 | 接着我们再来看下veth的实现。veth的相关ops实现为: 332 | 333 | ``` 334 | static const struct net_device_ops veth_netdev_ops = { 335 | .ndo_init = veth_dev_init, 336 | .ndo_open = veth_open, 337 | .ndo_stop = veth_close, 338 | .ndo_start_xmit = veth_xmit, 339 | .ndo_change_mtu = veth_change_mtu, 340 | .ndo_get_stats64 = veth_get_stats64, 341 | .ndo_set_rx_mode = veth_set_multicast_list, 342 | .ndo_set_mac_address = eth_mac_addr, 343 | #ifdef CONFIG_NET_POLL_CONTROLLER 344 | .ndo_poll_controller = veth_poll_controller, 345 | #endif 346 | .ndo_get_iflink = veth_get_iflink, 347 | }; 348 | ``` 349 | 350 | 所以我们看下veth_xmit的实现。现在我们的数据包准备从veth的某个端点发送出去了,然后sk_buff传到了veth_xmit,其实现为: 351 | 352 | ``` 353 | static netdev_tx_t veth_xmit(struct sk_buff *skb, struct net_device *dev) 354 | { 355 | struct veth_priv *priv = netdev_priv(dev); 356 | struct net_device *rcv; 357 | int length = skb->len; 358 | 359 | rcu_read_lock(); 360 | rcv = rcu_dereference(priv->peer); 361 | if (unlikely(!rcv)) { 362 | kfree_skb(skb); 363 | goto drop; 364 | } 365 | /* don't change ip_summed == CHECKSUM_PARTIAL, as that 366 | * will cause bad checksum on forwarded packets 367 | */ 368 | if (skb->ip_summed == CHECKSUM_NONE && 369 | rcv->features & NETIF_F_RXCSUM) 370 | skb->ip_summed = CHECKSUM_UNNECESSARY; 371 | 372 | if (likely(dev_forward_skb(rcv, skb) == NET_RX_SUCCESS)) { 373 | struct pcpu_vstats *stats = this_cpu_ptr(dev->vstats); 374 | 375 | u64_stats_update_begin(&stats->syncp); 376 | stats->bytes += length; 377 | stats->packets++; 378 | u64_stats_update_end(&stats->syncp); 379 | } else { 380 | drop: 381 | atomic64_inc(&priv->dropped); 382 | } 383 | rcu_read_unlock(); 384 | return NETDEV_TX_OK; 385 | } 386 | ``` 387 | 388 | 这里一般都会走dev_forward_skb函数。dev_forward_skb是内核提供的一个函数,我们在下面章节中讲bridge的时候会看到其主要功能。这里其用处是用于将某个数据包直接放某个CPU的softnet_data的input_queue上,并设置某个网卡来处理这个数据包(也就是内核直接让某个网卡来处理一个内存中的sk_buff包。可以简单的认为让某个网卡处理这个包要做的事情就是把这个网卡的net_device放到softnet_data的poll_list上然后将sk_buff的net_device的指针指向这个网卡的net_device)。于是这个被选中的网卡就会来处理这个包了。那么对于veth来说是哪个网卡被选中了做这个事情呢?从struct veth_priv *priv = netdev_priv(dev)和rcv = rcu_dereference(priv->peer)这两行代码就能知道这里是从veth的net_device的私有数据中找到对应的peer网卡,然后找到的peer网卡就是我们选中的目的网卡了。 389 | 390 | 391 | ### 多队列/RSS/RPS/RFS 392 | 393 | 在本章我们讲过了一个数据包的收包、发包路径。在相应的内容中笔者也提出过一个疑问给大家:一个数据包的收、发的过程是发生在哪个核上的?其实如果大家按照本章之前的内容来分析的话,会发现一个网卡的数据包基本上都是在一个核上进行收发的。比方说一个数据包到达了网卡,网卡触发中断通知某个CPU,然后接下来的事情就基本上都是这个CPU来处理了。我们知道对于一个物理机来说CPU的核数可能能有二三十个,但网卡的个数一般只有四块。那按照这个逻辑一个物理机上最多也就四个CPU能处理网络的包咯?对于Neutron的网络节点来说这个是不能接受的,因为这会让CPU严重成为性能提高的瓶颈。所以在这一节我们来讲下一个最常见的针对这个问题的优化方法。 394 | 395 | 这个方法的核心思想很简单:上面说的瓶颈之所以产生的原因我们知道是因为一个网卡对应一个中断向量号,而一个中断只能被一个CPU响应。那么能不能让这个网卡产生的中断分散到各个CPU上呢?一种方法就是多队列。这里的多队列指的是网卡本身硬件支持多队列,每个队列我们可以简单的看成就是一个能起到收发包作用的网卡,一个数据包到达网卡时,根据某个hash算法这个数据包会放到某个队列上,然后这个队列负责发送一个中断给CPU。假设一个网卡有24个队列,那么就可以绑定最多24个CPU,这样就能充分发挥CPU的使用率了。 396 | 397 | 我们来看下这种多队列的配置方法。内核开发者Robert Olsson写了一个脚本帮助大家简单的配置多队列,大家可以直接使用这个脚本进行配置: 398 | 399 | ``` 400 | # setting up irq affinity according to /proc/interrupts 401 | # 2008-11-25 Robert Olsson 402 | # 2009-02-19 updated by Jesse Brandeburg 403 | # 404 | # > Dave Miller: 405 | # (To get consistent naming in /proc/interrups) 406 | # I would suggest that people use something like: 407 | # char buf[IFNAMSIZ+6]; 408 | # 409 | # sprintf(buf, "%s-%s-%d", 410 | # netdev->name, 411 | # (RX_INTERRUPT ? "rx" : "tx"), 412 | # queue->index); 413 | # 414 | # Assuming a device with two RX and TX queues. 415 | # This script will assign: 416 | # 417 | # eth0-rx-0 CPU0 418 | # eth0-rx-1 CPU1 419 | # eth0-tx-0 CPU0 420 | # eth0-tx-1 CPU1 421 | # 422 | 423 | set_affinity() 424 | { 425 | MASK=$((1<<$VEC)) 426 | printf "%s mask=%X for /proc/irq/%d/smp_affinity\n" $DEV $MASK $IRQ 427 | printf "%X" $MASK > /proc/irq/$IRQ/smp_affinity 428 | #echo $DEV mask=$MASK for /proc/irq/$IRQ/smp_affinity 429 | #echo $MASK > /proc/irq/$IRQ/smp_affinity 430 | } 431 | 432 | if [ "$1" = "" ] ; then 433 | echo "Description:" 434 | echo " This script attempts to bind each queue of a multi-queue NIC" 435 | echo " to the same numbered core, ie tx0|rx0 --> cpu0, tx1|rx1 --> cpu1" 436 | echo "usage:" 437 | echo " $0 eth0 [eth1 eth2 eth3]" 438 | fi 439 | 440 | 441 | # check for irqbalance running 442 | IRQBALANCE_ON=`ps ax | grep -v grep | grep -q irqbalance; echo $?` 443 | if [ "$IRQBALANCE_ON" == "0" ] ; then 444 | echo " WARNING: irqbalance is running and will" 445 | echo " likely override this script's affinitization." 446 | echo " Please stop the irqbalance service and/or execute" 447 | echo " 'killall irqbalance'" 448 | fi 449 | 450 | # 451 | # Set up the desired devices. 452 | # 453 | 454 | for DEV in $* 455 | do 456 | for DIR in rx tx TxRx 457 | do 458 | MAX=`grep $DEV-$DIR /proc/interrupts | wc -l` 459 | if [ "$MAX" == "0" ] ; then 460 | MAX=`egrep -i "$DEV:.*$DIR" /proc/interrupts | wc -l` 461 | fi 462 | if [ "$MAX" == "0" ] ; then 463 | echo no $DIR vectors found on $DEV 464 | continue 465 | #exit 1 466 | fi 467 | for VEC in `seq 0 1 $MAX` 468 | do 469 | IRQ=`cat /proc/interrupts | grep -i $DEV-$DIR-$VEC"$" | cut -d: -f1 | sed "s/ //g"` 470 | if [ -n "$IRQ" ]; then 471 | set_affinity 472 | else 473 | IRQ=`cat /proc/interrupts | egrep -i $DEV:v$VEC-$DIR"$" | cut -d: -f1 | sed "s/ //g"` 474 | if [ -n "$IRQ" ]; then 475 | set_affinity 476 | fi 477 | fi 478 | done 479 | done 480 | done 481 | ``` 482 | 483 | 我们来看下关键的代码。关键的代码是: 484 | ``` 485 | IRQ=`cat /proc/interrupts | grep -i $DEV-$DIR-$VEC"$" | cut -d: -f1 | sed "s/ //g"` 486 | ``` 487 | 以及: 488 | ``` 489 | printf "%X" $MASK > /proc/irq/$IRQ/smp_affinity 490 | ``` 491 | 第一个关键代码的目的是获取网卡的某个队列其绑定的中断向量号,此时由于没有做别的什么操作,所以任何一个队列产生的数据包都只会发送到开机启动的时候系统所默认的CPU上,一般也就是CPU0。因此此时虽然有多队列但是所有队列依旧只和CPU0相关,这里我们要做的就是将某个队列从CPU0绑定到其他CPU上。这个队列可以通过其IRQ识别出,然后这个IRQ通过向/proc/irq/$IRQ/smp_affinity 写入一个掩码来将其和某个CPU绑定。掩码类似00000001这种,每一位表示一个CPU。比如00000001就表示CPU0。 492 | 493 | 另外脚本一开始还检查了irqrebalance是否运行,大家可以认为irqrebalance就是一个每个一定时间自动的设置队列和CPU绑定关系的程序。 494 | 495 | RSS(Receive Side Scaling),指的就是上面这种通过硬件多队列来提升CPU使用率进而优化性能的一种方法。 496 | 497 | 笔者所接触过的一些托管云用户有时候会使用一些老机器来做网络节点,这些机器的网卡很老,不支持硬件多队列,此时该如何优化呢?这就得用上RPS(Receive Packet Steering)了。什么是RPS呢?其实很简单,RPS就是软件实现的一种RSS。本来一个数据包到达网卡后走哪个队列是硬件决定的,现在由于硬件没有多队列所以硬件中断都是发生在一个CPU上。但是通过本书前面的学习我们知道真正的开销其实是softirq这个下半区中进行的,如果此时什么都不做的话,所有的开销都会发生在这个响应硬件中断的CPU上,此时这个CPU(一般是CPU0)就会不堪重负。但是通过RPS技术,硬件中断响应后内核会将这个sk_buff平衡的放到别的CPU的softnet_data的input_queue上,并且设置相应的softirq的标记,此时最耗时的softirq的工作就能由别的CPU负担了。这就是RPS技术的基本思想。 498 | 499 | 最后我们来看个RFS(Receive Flow Steering)。我们都知道内存在计算机中是一种金字塔结构,CPU的cache最快,然后是内存,然后是磁盘。如果一个数据流都能在一个CPU上进行处理那么就能充分利用cache。但是按照我们上面讲的RSS或者RPS,一个数据包走哪个CPU核是由一个hash算法决定的,这个hash算法一般只会通过源、目的IP或者mac来判断走哪个CPU。所以这个hash算法是看不到数据流(flow)层面的东西的。RFS可以看成是一个使用了高级点的hash算法的RSS,其保证同一个数据流的包都走同一个CPU。 500 | 501 | 最后如果大家对这些多队列的技术感兴趣的话可以看下https://www.kernel.org/doc/Documentation/networking/scaling.txt。链接中的文档包含了这些技术的说明、使用方法及相关原理。 502 | 503 | 504 | ### LSO/LRO/GSO/GRO/TSO/USO 505 | 506 | 首先先来讲一个术语:offload。这个在本书之前的内容中也提过,比如offload vxlan等。什么叫offload呢?很简单,就是将一个本来由软件实现的功能现在放到硬件上来实现。这里的offload vxlan的意思就是本来我vxlan的封包解包是由ovs来做的,offload后就由网卡或交换机帮我做了。 507 | 508 | LSO/LRO/GSO/GRO/TSO/USO等其实就是一种offload技术。先来说下全称: 509 | 510 | * LSO:Large Segment Offload 511 | * LRO:Large Receive Offload 512 | * GSO:Generic Segmentation Offload 513 | * GRO:Generic Receive Offload 514 | * TSO:TCP Segmentation Offload 515 | * USO:UDP Fragmentation offload 516 | 517 | 我们知道当数据包在传输的时候按照标准是必须分割成一个一个小于MTU的小包进行传输的,然后传输到另一头后另一头还得将这些数据包拼装回去。这些分割、拼装的事情都是软件实现的,于是就有人想将这些事情offload到硬件上去实现,接着就产生了上面的这些技术。 518 | 519 | * LSO:协议栈直接传递打包给网卡,由网卡负责分割 520 | * LRO:网卡对零散的小包进行拼装,返回给协议栈一个大包 521 | * GSO:LSO需要用户区分网卡是否支持该功能,GSO则会自动判断,如果支持则启用LSO,否则不启用 522 | * GRO:LRO需要用户区分网卡是否支持该功能,GRO则会自动判断,如果支持则启用LRO,否则不启用 523 | * TSO:针对TCP的分片的offload。类似LSO、GSO,但这里明确是针对TCP 524 | * USO:正对UDP的offload,一般是IP层面的分片处理 525 | 526 | 实际大家在使用中,必须做好充分的测试。 527 | 528 | 529 | ### DPDK 530 | 531 | 本章中说的收发包走的都是Linux内核提供的一套流程。而DPDK则不是。DPDK是一套Intel委托6wind开发的开源的数据收发库。当一个数据包进入网卡产生中断后,响应这个中断的驱动是DPDK安装的驱动。这个驱动会通过UIO机制直接让用户态可以直接操作这个数据包。在用户态用户可以写一个程序通过DPDK提供的API处理这个数据包,比如直接在用户态写一个二层转发实现,或者在用户态直接实现一个vRouter等。用户态的好处是其可以充分利用CPU。内核态的协议栈存在一个问题:处理包的性能和CPU核数不成正比。当CPU的核数增加到一定个数后,再增加CPU核数对于数据包的收发作用就不大了。因此大的公司可以基于DPDK直接在用户态实现一个全新的协议栈程序,只实现自己需要的功能,显而易见这个协议栈程序肯定会是一个多线程的程序用于充分挖掘多核CPU的潜力。 532 | 533 | 对于存在性能瓶颈且使用了很多方法优化都没法提高性能但又不希望使用硬件方案的用户来说,使用基于DPDK或类似技术实现的协议栈是一个可以考虑的选择。 534 | 535 | 536 | -------------------------------------------------------------------------------- /book/ch05/s5.3.md: -------------------------------------------------------------------------------- 1 | ## 网络namespace的实现 2 | 3 | 在Neutron的纯软件方案中,网络的namespace起到了非常重要的作用。比如Neutorn中的路由器就是通过一个独立的namespace配合其中的路由、iptables等来实现的。这里我们来将一下网络namespace的实现。 4 | 5 | 关于namespace可以参考下下面的文章: 6 | 7 | * http://coolshell.cn/articles/17010.html 8 | * http://coolshell.cn/articles/17029.html 9 | 10 | 传统上如果我们要在一台物理机上隔离出一个独立的环境,在其中运行某个服务的时候,一般都是通过软件虚拟化的方式实现的。比如笔者需要在一台物理机上建立一个虚拟路由器,可以通过: 11 | 12 | 1. 建立一台虚拟机,拥有两个虚拟网卡 13 | 2. 在虚拟机中配置好路由表 14 | 15 | 此时这个虚拟机就能看成是一个拥有两个物理口的路由器了,每个虚拟网卡可以外接一个子网,实现子网间路由。但这样做有一些缺陷,主要是: 16 | 17 | 1. 虚拟化后的性能没有直接在物理机上的性能好 18 | 2. 启动、停止一个虚拟机耗时较长 19 | 20 | 于是在很久之前就有人在Unix、Linux上实现namespace这么一个功能了。虚拟化能满足需求,但是开销太大,那就不用虚拟化,在代码层面上做个隔离嘛。所以namespace在实现上最重要的就是做了这个隔离。下面这个例子可以帮助大家在代码层面理解namespace的实现。在没有引入namespace之前,获取当前系统上网卡名字的接口可能是类似于下面的实现: 21 | 22 | ``` 23 | interfaces = ["eth0", "eth1"] 24 | 25 | def get_interfaces(): 26 | return interfaces 27 | ``` 28 | 29 | 在有了namespace后,实现变为: 30 | 31 | ``` 32 | class NS(object): 33 | 34 | def __init__(): 35 | self.interfaces = [] 36 | 37 | def add_interface(ifname): 38 | self.interfaces.append(ifname) 39 | 40 | def get_interfaces(): 41 | return self.interfaces 42 | 43 | def get_interfaces(ns): 44 | return ns.get_interfaces() 45 | 46 | ns_apple = NS() 47 | ns_apple.add_interface("eth0") 48 | ns_apple.add_interface("eth1") 49 | 50 | ns_pear = NS() 51 | ns_pear.add_interface("eth0") 52 | 53 | # apple namespace下的网卡列表 54 | apple_ifs = get_interfaces(ns_apple) 55 | # pear namespace下的网卡列表 56 | pear_ifs = get_interfaces(ns_pear) 57 | 58 | ``` 59 | 60 | 是不是很简单?确实内核如果要支持namespace的话,主要的改动就是在以前老的函数中增加一个namespace的参数代表这个函数会在哪个namespace下进行操作。对于内核社区来说增加namespace的很大的一个工作量就是修改所有涉及到的函数。 61 | 62 | 我们来看网络部分的namespace的实现。上面的例子中一个NS对象就是一个namespace,对于内核来说,一个进程如何知道自己在哪个namespace下操作是依赖进程task_struct的nsproxy属性来判断的: 63 | 64 | ``` 65 | /* namespaces */ 66 | struct nsproxy *nsproxy; 67 | 68 | /* 69 | * A structure to contain pointers to all per-process 70 | * namespaces - fs (mount), uts, network, sysvipc, etc. 71 | * 72 | * The pid namespace is an exception -- it's accessed using 73 | * task_active_pid_ns. The pid namespace here is the 74 | * namespace that children will use. 75 | * 76 | * 'count' is the number of tasks holding a reference. 77 | * The count for each namespace, then, will be the number 78 | * of nsproxies pointing to it, not the number of tasks. 79 | * 80 | * The nsproxy is shared by tasks which share all namespaces. 81 | * As soon as a single namespace is cloned or unshared, the 82 | * nsproxy is copied. 83 | */ 84 | struct nsproxy { 85 | atomic_t count; 86 | struct uts_namespace *uts_ns; 87 | struct ipc_namespace *ipc_ns; 88 | struct mnt_namespace *mnt_ns; 89 | struct pid_namespace *pid_ns_for_children; 90 | struct net *net_ns; 91 | }; 92 | extern struct nsproxy init_nsproxy; 93 | ``` 94 | 95 | 我们可以把nsproxy简单的类比成我们上面举例的NS类。只不过内核对nsproxy做了细分,分成了uts、ipc、mnt、pid以及net这几个namespace,我们的网络namespace就是这里的net。也就是说原来的代码一个进程如果想要查看当前环境下的网卡列表是通过调用get_interfaces查看的获取的话,此时则是通过get_interfaces(task_struct->nsproxy->net_ns)来获取了。 96 | 97 | 来看下net_ns。这个结构体在include/net/net_namespace.h文件中。下面是其一些属性: 98 | 99 | ``` 100 | struct net { 101 | ... 102 | //namespace的链表 103 | struct list_head list; 104 | //用来串net_device的链表 105 | struct list_head dev_base_head; 106 | struct hlist_head *dev_name_head; 107 | struct hlist_head *dev_index_head; 108 | //每个namespace自己的lo链表 109 | struct net_device *loopback_dev; 110 | ... 111 | }; 112 | ``` 113 | 114 | 在本书的前面章节介绍了通过ip命令来操作网络namespace的相关命令。下面我们通过ip命令的源码来看下网络namespace的一些实现。当敲下ip netns add XXX后,代码会进入到: 115 | 116 | ``` 117 | if (matches(*argv, "add") == 0) 118 | return netns_add(argc-1, argv+1); 119 | ``` 120 | 121 | 这里netns_add负责创建一个网络namespace,实现为: 122 | 123 | ``` 124 | static int netns_add(int argc, char **argv) 125 | { 126 | /* This function creates a new network namespace and 127 | * a new mount namespace and bind them into a well known 128 | * location in the filesystem based on the name provided. 129 | * 130 | * The mount namespace is created so that any necessary 131 | * userspace tweaks like remounting /sys, or bind mounting 132 | * a new /etc/resolv.conf can be shared between uers. 133 | */ 134 | char netns_path[MAXPATHLEN]; 135 | const char *name; 136 | int fd; 137 | int made_netns_run_dir_mount = 0; 138 | 139 | if (argc < 1) { 140 | fprintf(stderr, "No netns name specified\n"); 141 | return -1; 142 | } 143 | name = argv[0]; 144 | 145 | snprintf(netns_path, sizeof(netns_path), "%s/%s", NETNS_RUN_DIR, name); 146 | 147 | if (create_netns_dir()) 148 | return -1; 149 | ``` 150 | 151 | 首先这里会在NETNS_RUN_DIR下建立一个目录,NETNS_RUN_DIR的定义为: 152 | 153 | ``` 154 | #define NETNS_RUN_DIR "/var/run/netns" 155 | ``` 156 | 157 | 比如这个例子,我们可以看到ip命令为我们建立了目录: 158 | 159 | ``` 160 | [root@dev ~]# ip netns add X 161 | [root@dev ~]# cd /var/run/netns/ 162 | [root@dev netns]# ll 163 | 总用量 0 164 | -r--r--r--. 1 root root 0 7月 4 21:11 X 165 | ``` 166 | 167 | 此时netns_add还没有为我们真正建立namespace,所以我们继续分析其实现(这里会跳过mnt部分): 168 | 169 | ``` 170 | /* Create the filesystem state */ 171 | fd = open(netns_path, O_RDONLY|O_CREAT|O_EXCL, 0); 172 | if (fd < 0) { 173 | fprintf(stderr, "Cannot create namespace file \"%s\": %s\n", 174 | netns_path, strerror(errno)); 175 | return -1; 176 | } 177 | close(fd); 178 | if (unshare(CLONE_NEWNET) < 0) { 179 | fprintf(stderr, "Failed to create a new network namespace \"%s\": %s\n", 180 | name, strerror(errno)); 181 | goto out_delete; 182 | } 183 | ``` 184 | 185 | 注意这里的unshare以及传给它的CLONE_NEWNET,unshare由内核提供,其会建立一个新的网络namespace并将当前进程的task_struct->nsproxy->net_ns指向它。因此从此刻开始这个进程所执行的所有设计网络相关的命令就都在这个新的namespace中了。至于unshare建立新的namespace会做些什么操作我们下面会看到。 186 | 187 | 另外这里读者可能会有个疑问,如果unshare会让task_struct->nsproxy->net_ns指向一个新的namespace,那么原来task_struct->nsproxy->net_ns指向的是什么呢?如果原来task_struct->nsproxy->net_ns指向的也是一个namespace,比如namespace A,那么为什么ip netns list命令看不到这个namespace而只能看到新创建的namespace呢?其实这里是被ip命令欺骗了,其list命令的实现为: 188 | 189 | ``` 190 | static int netns_list(int argc, char **argv) 191 | { 192 | struct dirent *entry; 193 | DIR *dir; 194 | int id; 195 | 196 | dir = opendir(NETNS_RUN_DIR); 197 | if (!dir) 198 | return 0; 199 | 200 | while ((entry = readdir(dir)) != NULL) { 201 | if (strcmp(entry->d_name, ".") == 0) 202 | continue; 203 | if (strcmp(entry->d_name, "..") == 0) 204 | continue; 205 | printf("%s", entry->d_name); 206 | if (ipnetns_have_nsid()) { 207 | id = get_netnsid_from_name(entry->d_name); 208 | if (id >= 0) 209 | printf(" (id: %d)", id); 210 | } 211 | printf("\n"); 212 | } 213 | closedir(dir); 214 | return 0; 215 | } 216 | ``` 217 | 218 | 可以看到,ip netns list只会列出NETNS_RUN_DIR下的那些namespace,而不会列出内核中所有的namespace。其实内核在启动的时候会建立一个默认的网络namespace DEFAULT_NETNS,如果没有特别操作的话,进程一般都是使用这个DEFAULT_NETNS作为网络的namespace的。也就是task_struct->nsproxy->net_ns一般都是指向DEFAULT_NETNS。 219 | 220 | 现在我们知道新建立一个网络namespace可以通过unshare来实现。我们来看下一个新的网络namespace的建立代码。切入点为: 221 | 222 | ``` 223 | int copy_namespaces(unsigned long flags, struct task_struct *tsk) 224 | { 225 | struct nsproxy *old_ns = tsk->nsproxy; 226 | struct user_namespace *user_ns = task_cred_xxx(tsk, user_ns); 227 | struct nsproxy *new_ns; 228 | 229 | if (likely(!(flags & (CLONE_NEWNS | CLONE_NEWUTS | CLONE_NEWIPC | 230 | CLONE_NEWPID | CLONE_NEWNET)))) { 231 | get_nsproxy(old_ns); 232 | return 0; 233 | } 234 | 235 | if (!ns_capable(user_ns, CAP_SYS_ADMIN)) 236 | return -EPERM; 237 | 238 | /* 239 | * CLONE_NEWIPC must detach from the undolist: after switching 240 | * to a new ipc namespace, the semaphore arrays from the old 241 | * namespace are unreachable. In clone parlance, CLONE_SYSVSEM 242 | * means share undolist with parent, so we must forbid using 243 | * it along with CLONE_NEWIPC. 244 | */ 245 | if ((flags & (CLONE_NEWIPC | CLONE_SYSVSEM)) == 246 | (CLONE_NEWIPC | CLONE_SYSVSEM)) 247 | return -EINVAL; 248 | 249 | new_ns = create_new_namespaces(flags, tsk, user_ns, tsk->fs); 250 | if (IS_ERR(new_ns)) 251 | return PTR_ERR(new_ns); 252 | 253 | tsk->nsproxy = new_ns; 254 | return 0; 255 | } 256 | ``` 257 | create_new_namespaces建立了新的namespace。来看下涉及网络的部分: 258 | 259 | ``` 260 | new_nsp->net_ns = copy_net_ns(flags, user_ns, tsk->nsproxy->net_ns); 261 | if (IS_ERR(new_nsp->net_ns)) { 262 | err = PTR_ERR(new_nsp->net_ns); 263 | goto out_net; 264 | } 265 | 266 | struct net *copy_net_ns(unsigned long flags, 267 | struct user_namespace *user_ns, struct net *old_net) 268 | { 269 | struct net *net; 270 | int rv; 271 | 272 | if (!(flags & CLONE_NEWNET)) 273 | return get_net(old_net); 274 | 275 | net = net_alloc(); 276 | if (!net) 277 | return ERR_PTR(-ENOMEM); 278 | 279 | get_user_ns(user_ns); 280 | 281 | mutex_lock(&net_mutex); 282 | rv = setup_net(net, user_ns); 283 | if (rv == 0) { 284 | rtnl_lock(); 285 | list_add_tail_rcu(&net->list, &net_namespace_list); 286 | rtnl_unlock(); 287 | } 288 | mutex_unlock(&net_mutex); 289 | if (rv < 0) { 290 | put_user_ns(user_ns); 291 | net_drop_ns(net); 292 | return ERR_PTR(rv); 293 | } 294 | return net; 295 | } 296 | ``` 297 | 298 | 如果CLONE_NEWNET没有设置,那么就用老的网络的namespace(也就是新的进程继承老的进程的namespace,你老的能看到几个网卡我新的同样能看到,因为我们都是指向同一个结构体)。否则则是先调用net_alloc获取个新的net结构体,然后通过setup_net初始化后将它放到内核全局的net_namespace_list链表下。net_namespace_list定义为: 299 | 300 | ``` 301 | LIST_HEAD(net_namespace_list); 302 | EXPORT_SYMBOL_GPL(net_namespace_list); 303 | ``` 304 | 305 | 所有对于一个新的网络的namespace,初始化工作主要由setup_net实现。其代码为: 306 | 307 | ``` 308 | /* 309 | * setup_net runs the initializers for the network namespace object. 310 | */ 311 | static __net_init int setup_net(struct net *net, struct user_namespace *user_ns) 312 | { 313 | /* Must be called with net_mutex held */ 314 | const struct pernet_operations *ops, *saved_ops; 315 | int error = 0; 316 | LIST_HEAD(net_exit_list); 317 | 318 | atomic_set(&net->count, 1); 319 | atomic_set(&net->passive, 1); 320 | net->dev_base_seq = 1; 321 | net->user_ns = user_ns; 322 | idr_init(&net->netns_ids); 323 | 324 | list_for_each_entry(ops, &pernet_list, list) { 325 | error = ops_init(ops, net); 326 | if (error < 0) 327 | goto out_undo; 328 | } 329 | out: 330 | return error; 331 | 332 | out_undo: 333 | /* Walk through the list backwards calling the exit functions 334 | * for the pernet modules whose init functions did not fail. 335 | */ 336 | list_add(&net->exit_list, &net_exit_list); 337 | saved_ops = ops; 338 | list_for_each_entry_continue_reverse(ops, &pernet_list, list) 339 | ops_exit_list(ops, &net_exit_list); 340 | 341 | ops = saved_ops; 342 | list_for_each_entry_continue_reverse(ops, &pernet_list, list) 343 | ops_free_list(ops, &net_exit_list); 344 | 345 | rcu_barrier(); 346 | goto out; 347 | } 348 | ``` 349 | 350 | 这里主要是调用了pernet_list的ops来执行各种初始化。内核中一些子系统、模块可以通过register_pernet_device注册一些需要在一个新的namespace建立时执行的函数放在parent_list上,此时一个新的网络namespace被建立的时候就能调用这些函数。比如在net_dev_init这里可以看到如下代码: 351 | 352 | ``` 353 | /* The loopback device is special if any other network devices 354 | * is present in a network namespace the loopback device must 355 | * be present. Since we now dynamically allocate and free the 356 | * loopback device ensure this invariant is maintained by 357 | * keeping the loopback device as the first device on the 358 | * list of network devices. Ensuring the loopback devices 359 | * is the first device that appears and the last network device 360 | * that disappears. 361 | */ 362 | if (register_pernet_device(&loopback_net_ops)) 363 | goto out; 364 | ``` 365 | loopback_net_ops会在新的namespace中建立一个lo的net_device,所以我们的namespace中就都能看到lo设备的存在。 366 | 367 | 需要说明的是虽然上面的分析都是通过进程的task_struct来引到网络的namespace结构体上,但是实际上这个结构体是独立存在在内核中的。比如net_device有一个nd_net指针指向了某个网络的namespace结构体,表示这个net_device属于哪个namespace。 368 | 369 | 网络namespace的基本实现原理就是上面这些。最后有一个需要注意的就是目前还不是所有网络相关的代码都能感知到namespace的存在。也就是说一些代码目前还没有来得及改成本节开始举例中有ns参数的形式。笔者曾经遇到过一个物理机的ip_forward系统变量设置为1但是namespace中依然为0造成的故障,现在我们知道这个故障的原因是对于ip_forward这个系统变量在网络的namespace中已经支持造成的。如果网络的namespace还不能支持系统变量,那么类似于get_system_var这种函数就还不能传递ns参数,于是所有namespace取到的系统参数应该都是同一个,也就不会存在ip_forward不一致的情况了。那么如何快速简单粗略的定位目前内核的网络namespace代码被哪些模块感知了呢?可以查看网络namespace的net这个结构体的属性,比如net结构体里有sysctl相关的结构体: 370 | 371 | ``` 372 | struct netns_ipv4 ipv4; 373 | 374 | struct netns_ipv4 { 375 | #ifdef CONFIG_SYSCTL 376 | struct ctl_table_header *forw_hdr; 377 | struct ctl_table_header *frags_hdr; 378 | struct ctl_table_header *ipv4_hdr; 379 | struct ctl_table_header *route_hdr; 380 | struct ctl_table_header *xfrm4_hdr; 381 | #endif 382 | ``` 383 | 384 | 因此我们相关系统变量能够感知到网络namespace的存在。 -------------------------------------------------------------------------------- /book/ch05/s5.4.md: -------------------------------------------------------------------------------- 1 | ## Linux Bridge的实现 2 | 3 | 我们来看一下Linux Bridge在内核中的实现的基本原理及代码。在Neutron中,Linux Bridge目前主要用在虚拟机和br-int之间,起到一个承载安全组规则的作用。当然对于一些简单的环境也可以直接使用Linux Bridge来取代ovs。 4 | 5 | 在本章前面讲收包流程的时候讲到过,当一个数据包进来后,poll函数会通过netif_receive_skb进行下一步的处理。在提交给上层协议栈前,会先看是不是要走bridge的逻辑,如果要走的话就会由bridge的逻辑进行处理,而不是往上层继续调用。加入此时受到数据包的网卡eth0被放到了一个bridge中,则就会走bridge的逻辑。 6 | 7 | 这里需要注意的一点是如果一个网卡属于了一个bridge,但是其自己又配了一个IP地址,那么对于发包这样做是没有影响的,但是对于收包就要决定这个包是交给bridge处理还是交给IP层处理,这个决定会由ebtables决定,感兴趣的读者可以去学习下。 8 | 9 | 我们来看bridge的实现,bridge也是以模块形式实现在内核中的,只不过一般内核在编译的时候都直接编译进去了,因此不需要大家手工加载。我们来看下这个bridge模块的入口: 10 | 11 | ``` 12 | module_init(br_init) 13 | module_exit(br_deinit) 14 | ``` 15 | 16 | 对于br_init的实现,其主要做了下面的事情: 17 | 18 | 1. 初始化转发表 19 | 2. 设置ioctl对应的br_ioctl_hook 20 | 3. 初始化BPDUs相关的函数指针 21 | 4. 在netdev_chain中注册个回调 22 | 23 | 其中ioctl对应的实现为: 24 | 25 | ``` 26 | int br_ioctl_deviceless_stub(struct net *net, unsigned int cmd, void __user *uarg) 27 | { 28 | switch (cmd) { 29 | case SIOCGIFBR: 30 | case SIOCSIFBR: 31 | return old_deviceless(net, uarg); 32 | 33 | case SIOCBRADDBR: 34 | case SIOCBRDELBR: 35 | { 36 | char buf[IFNAMSIZ]; 37 | 38 | if (!ns_capable(net->user_ns, CAP_NET_ADMIN)) 39 | return -EPERM; 40 | 41 | if (copy_from_user(buf, uarg, IFNAMSIZ)) 42 | return -EFAULT; 43 | 44 | buf[IFNAMSIZ-1] = 0; 45 | if (cmd == SIOCBRADDBR) 46 | return br_add_bridge(net, buf); 47 | 48 | return br_del_bridge(net, buf); 49 | } 50 | } 51 | return -EOPNOTSUPP; 52 | } 53 | ``` 54 | 55 | 这里会看到br_add_bridge和br_del_bridge,当我们用brctl命令建立或删除bridge的时候,brctl就是通过ioctl调用这两个函数来实现对应操作的。其实现我们后面会讲到。我们回到br_init代码中,在其代码里可以看到: 56 | 57 | ``` 58 | err = stp_proto_register(&br_stp_proto); 59 | if (err < 0) { 60 | pr_err("bridge: can't register sap for STP\n"); 61 | return err; 62 | } 63 | 64 | err = br_fdb_init(); 65 | if (err) 66 | goto err_out; 67 | 68 | err = register_pernet_subsys(&br_net_ops); 69 | if (err) 70 | goto err_out1; 71 | 72 | err = br_nf_core_init(); 73 | if (err) 74 | goto err_out2; 75 | 76 | err = register_netdevice_notifier(&br_device_notifier); 77 | if (err) 78 | goto err_out3; 79 | 80 | err = register_netdev_switch_notifier(&br_netdev_switch_notifier); 81 | if (err) 82 | goto err_out4; 83 | 84 | err = br_netlink_init(); 85 | if (err) 86 | goto err_out5; 87 | 88 | brioctl_set(br_ioctl_deviceless_stub); 89 | ``` 90 | 这里初始化了很多bridge相关的代码。stp_proto_register是STP相关的初始化,STP的作用主要是防止网络中出现环路。br_fdb_init初始化了一个forward db,这个forward db的作用就是记录bridge的某个口其对应了哪些mac地址,也就是我们通常说的交换机转发表。而br_netlink_init则建立了netlink层的一些函数指针: 91 | 92 | ``` 93 | int __init br_netlink_init(void) 94 | { 95 | int err; 96 | 97 | br_mdb_init(); 98 | rtnl_af_register(&br_af_ops); 99 | 100 | err = rtnl_link_register(&br_link_ops); 101 | if (err) 102 | goto out_af; 103 | 104 | return 0; 105 | 106 | out_af: 107 | rtnl_af_unregister(&br_af_ops); 108 | br_mdb_uninit(); 109 | return err; 110 | } 111 | ``` 112 | 113 | 现在来看下一个bridge的建立流程,在内核中一个bridge的体现也是一个net_device结构体,由于是虚拟设备,所以private的字段会比较重要。根据上面看的,我们建立一个bridge的时候入口从br_add_bridge开始看: 114 | 115 | ``` 116 | int br_add_bridge(struct net *net, const char *name) 117 | { 118 | struct net_device *dev; 119 | int res; 120 | 121 | dev = alloc_netdev(sizeof(struct net_bridge), name, NET_NAME_UNKNOWN, 122 | br_dev_setup); 123 | 124 | if (!dev) 125 | return -ENOMEM; 126 | 127 | dev_net_set(dev, net); 128 | dev->rtnl_link_ops = &br_link_ops; 129 | 130 | res = register_netdev(dev); 131 | if (res) 132 | free_netdev(dev); 133 | return res; 134 | } 135 | 136 | ``` 137 | 138 | 这里代码一眼就能看出我们要关注的东西:br_dev_setup、dev_net_set以及br_link_ops。 139 | 140 | br_dev_setup这个根据之前文章讲的对于alloc_netdev分析,我们知道其主要会初始化net_device的私有属性。代码如下: 141 | 142 | ··· 143 | void br_dev_setup(struct net_device *dev) 144 | { 145 | struct net_bridge *br = netdev_priv(dev); 146 | 147 | eth_hw_addr_random(dev); 148 | ether_setup(dev); 149 | 150 | dev->netdev_ops = &br_netdev_ops; 151 | dev->destructor = br_dev_free; 152 | dev->ethtool_ops = &br_ethtool_ops; 153 | SET_NETDEV_DEVTYPE(dev, &br_type); 154 | dev->tx_queue_len = 0; 155 | dev->priv_flags = IFF_EBRIDGE; 156 | 157 | dev->features = COMMON_FEATURES | NETIF_F_LLTX | NETIF_F_NETNS_LOCAL | 158 | NETIF_F_HW_VLAN_CTAG_TX | NETIF_F_HW_VLAN_STAG_TX; 159 | dev->hw_features = COMMON_FEATURES | NETIF_F_HW_VLAN_CTAG_TX | 160 | NETIF_F_HW_VLAN_STAG_TX; 161 | dev->vlan_features = COMMON_FEATURES; 162 | 163 | br->dev = dev; 164 | spin_lock_init(&br->lock); 165 | INIT_LIST_HEAD(&br->port_list); 166 | spin_lock_init(&br->hash_lock); 167 | 168 | br->bridge_id.prio[0] = 0x80; 169 | br->bridge_id.prio[1] = 0x00; 170 | 171 | ether_addr_copy(br->group_addr, eth_reserved_addr_base); 172 | 173 | br->stp_enabled = BR_NO_STP; 174 | br->group_fwd_mask = BR_GROUPFWD_DEFAULT; 175 | br->group_fwd_mask_required = BR_GROUPFWD_DEFAULT; 176 | 177 | br->designated_root = br->bridge_id; 178 | br->bridge_max_age = br->max_age = 20 * HZ; 179 | br->bridge_hello_time = br->hello_time = 2 * HZ; 180 | br->bridge_forward_delay = br->forward_delay = 15 * HZ; 181 | br->ageing_time = 300 * HZ; 182 | 183 | br_netfilter_rtable_init(br); 184 | br_stp_timer_init(br); 185 | br_multicast_init(br); 186 | } 187 | ··· 188 | 189 | 首先可以看到一个bridge其实是个以太网设备(ether_setup)。然后的代码是一些函数指针的初始化,这个和一般的net_device没有啥区别。然后的代码是对br这个指向私有内存段的指针做相关的赋值。 190 | 191 | 在函数指针中我们先熟悉下bridge的几个赋值内容,混个眼熟: 192 | 193 | ``` 194 | static const struct net_device_ops br_netdev_ops = { 195 | .ndo_open = br_dev_open, 196 | .ndo_stop = br_dev_stop, 197 | .ndo_init = br_dev_init, 198 | .ndo_start_xmit = br_dev_xmit, 199 | .ndo_get_stats64 = br_get_stats64, 200 | .ndo_set_mac_address = br_set_mac_address, 201 | .ndo_set_rx_mode = br_dev_set_multicast_list, 202 | .ndo_change_rx_flags = br_dev_change_rx_flags, 203 | .ndo_change_mtu = br_change_mtu, 204 | .ndo_do_ioctl = br_dev_ioctl, 205 | #ifdef CONFIG_NET_POLL_CONTROLLER 206 | .ndo_netpoll_setup = br_netpoll_setup, 207 | .ndo_netpoll_cleanup = br_netpoll_cleanup, 208 | .ndo_poll_controller = br_poll_controller, 209 | #endif 210 | .ndo_add_slave = br_add_slave, 211 | .ndo_del_slave = br_del_slave, 212 | .ndo_fix_features = br_fix_features, 213 | .ndo_fdb_add = br_fdb_add, 214 | .ndo_fdb_del = br_fdb_delete, 215 | .ndo_fdb_dump = br_fdb_dump, 216 | .ndo_bridge_getlink = br_getlink, 217 | .ndo_bridge_setlink = br_setlink, 218 | .ndo_bridge_dellink = br_dellink, 219 | }; 220 | ``` 221 | 222 | 再来看看一个port是怎么被加入到bridge中的,加port的代码为br_add_if(bridge的net_device中的ioctl指针,即br_dev_ioctl会负责调用)。主要做了下面几件事情: 223 | 224 | 1. 判断这个port是不是一个以太网设备,如果不是则失败 225 | 2. 判断这个port是不是一个bridge,如果是则失败 226 | 3. 判断这个port是否已经属于某个bridge,如果是则失败 227 | 4. 分配一个ID号 228 | 5. 分配默认的priority 229 | 6. 生成PORT ID 230 | 7. 根据port所对应的device的速率生成默认的cost 231 | 8. 设置状态 232 | 9. 将这个port对应的net_device结构和bridge的net_device关联 233 | 234 | 对于最后一步中的关联,在linux中所有的bridge上的port都是以链表形式串在一起的,链表的头在bridge的net_device的私有数据中。同时在port被加入的时候其MAC地址就被加入了转发表中,同时会打开混杂模式。 235 | 236 | 下面来看下数据包的接收,内核中这里的代码和netfilter的hook是紧密关联的,我们熟悉的iptables就是基于netfilter实现的。 237 | 238 | 来看收包,一个包到达后会走netif_receive_skb,后者有如下代码: 239 | 240 | ``` 241 | rx_handler = rcu_dereference(skb->dev->rx_handler); 242 | if (rx_handler) { 243 | if (pt_prev) { 244 | ret = deliver_skb(skb, pt_prev, orig_dev); 245 | pt_prev = NULL; 246 | } 247 | switch (rx_handler(&skb)) { 248 | case RX_HANDLER_CONSUMED: 249 | ret = NET_RX_SUCCESS; 250 | goto unlock; 251 | case RX_HANDLER_ANOTHER: 252 | goto another_round; 253 | case RX_HANDLER_EXACT: 254 | deliver_exact = true; 255 | case RX_HANDLER_PASS: 256 | break; 257 | default: 258 | BUG(); 259 | } 260 | } 261 | ``` 262 | 263 | skb->dev->rx_handler是在br_add_if这个添加port端口的时候给对应的dev赋值的,也就是说如果有个port是一个bridge的port了,那么通过其的sk_buff对象的dev->rx_handler就是被赋值的了,于是收包的时候就会走bridge的逻辑。赋值的内容为: 264 | 265 | ··· 266 | err = netdev_rx_handler_register(dev, br_handle_frame, p); 267 | ··· 268 | 269 | br_handle_frame的基本逻辑录下: 270 | 1. 判断port的状态,如果disable了则drop数据包。如果是FORWARDING/LEARNING则学习相关信息更新转发表。 271 | 2. 判断STP是否enable,我们这边看简单的没有启动STP的 272 | 3. 如果port状态不是FORWARDING则丢弃 273 | 4. 走ebtables的逻辑,判断是routing还是bridging。如果是routing则直接返回 274 | 5. 判断目的地址是不是接收设备,如果是的话设置pkt type为PACKET_HOST,然后走netfile的NF_BR_PRE_ROUTING这个hook 275 | 6. NF_BR_PRE_ROUTING返回可以继续处理这个包,则调用br_handle_frame_finish(如果我们走STP的路则此时会调用br_stp_handle_bpdu): 276 | 277 | ``` 278 | forward: 279 | switch (p->state) { 280 | case BR_STATE_FORWARDING: 281 | rhook = rcu_dereference(br_should_route_hook); 282 | if (rhook) { 283 | if ((*rhook)(skb)) { 284 | *pskb = skb; 285 | return RX_HANDLER_PASS; 286 | } 287 | dest = eth_hdr(skb)->h_dest; 288 | } 289 | /* fall through */ 290 | case BR_STATE_LEARNING: 291 | if (ether_addr_equal(p->br->dev->dev_addr, dest)) 292 | skb->pkt_type = PACKET_HOST; 293 | 294 | NF_HOOK(NFPROTO_BRIDGE, NF_BR_PRE_ROUTING, NULL, skb, 295 | skb->dev, NULL, 296 | br_handle_frame_finish); 297 | break; 298 | default: 299 | drop: 300 | kfree_skb(skb); 301 | } 302 | return RX_HANDLER_CONSUMED; 303 | 304 | ``` 305 | 306 | 对于br_handle_frame_finish,其会做如下的事情: 307 | 1. 通过br_fdb_update更新转发表 308 | 2. 根据目的mac查找转发表找对应的port,如果找到则调用br_forward,否则调用br_flood_frame广播。 309 | 3. 如果设备处于混杂模式,或者目的mac地址就是这个port或应该走这个port,则调用br_pass_frame_up 310 | 311 | 以上三步就能说明一个入包的大致逻辑了。当网卡收到一个数据包的时候,由于这个卡的net_device是bridge的一个port,所以这个数据包的sk_buff-dev-rx_handler不为NULL,所以会走bridge的逻辑,然后通过查表发现要发给另一个此bridge下的port,于是调用br_forward发给这个port。然后另一个port的netif_receive_skb会的被调用,此时会走第三步,于是调用br_pass_frame_up,而此时就会走上层协议栈了。 312 | 313 | 另外如果一个数据包直接发给了bridge,则会调用其net_device绑定的发包函数,也就是br_dev_xmit。后者的逻辑和普通的port一样,首先查转发表,查到的话发个这个port,否则广播。 314 | 315 | 如果一个数据包直接发给了bridge,则会调用其net_device绑定的发包函数,也就是br_dev_xmit。后者的逻辑和普通的port一样,首先查转发表,查到的话发个这个port,否则广播。来看下代码: 316 | 317 | ``` 318 | int br_handle_frame_finish(struct sock *sk, struct sk_buff *skb) 319 | { 320 | const unsigned char *dest = eth_hdr(skb)->h_dest; 321 | struct net_bridge_port *p = br_port_get_rcu(skb->dev); 322 | struct net_bridge *br; 323 | struct net_bridge_fdb_entry *dst; 324 | struct net_bridge_mdb_entry *mdst; 325 | struct sk_buff *skb2; 326 | bool unicast = true; 327 | u16 vid = 0; 328 | 329 | if (!p || p->state == BR_STATE_DISABLED) 330 | goto drop; 331 | 332 | if (!br_allowed_ingress(p->br, nbp_get_vlan_info(p), skb, &vid)) 333 | goto out; 334 | 335 | /* insert into forwarding database after filtering to avoid spoofing */ 336 | br = p->br; 337 | if (p->flags & BR_LEARNING) 338 | br_fdb_update(br, p, eth_hdr(skb)->h_source, vid, false); 339 | 340 | if (!is_broadcast_ether_addr(dest) && is_multicast_ether_addr(dest) && 341 | br_multicast_rcv(br, p, skb, vid)) 342 | goto drop; 343 | 344 | if (p->state == BR_STATE_LEARNING) 345 | goto drop; 346 | 347 | BR_INPUT_SKB_CB(skb)->brdev = br->dev; 348 | ``` 349 | 350 | 首先是状态的判断以及转发表的操作。接着: 351 | 352 | ``` 353 | /* The packet skb2 goes to the local host (NULL to skip). */ 354 | skb2 = NULL; 355 | 356 | if (br->dev->flags & IFF_PROMISC) 357 | skb2 = skb; 358 | 359 | dst = NULL; 360 | 361 | if (IS_ENABLED(CONFIG_INET) && skb->protocol == htons(ETH_P_ARP)) 362 | br_do_proxy_arp(skb, br, vid, p); 363 | 364 | if (is_broadcast_ether_addr(dest)) { 365 | skb2 = skb; 366 | unicast = false; 367 | } else if (is_multicast_ether_addr(dest)) { 368 | mdst = br_mdb_get(br, skb, vid); 369 | if ((mdst || BR_INPUT_SKB_CB_MROUTERS_ONLY(skb)) && 370 | br_multicast_querier_exists(br, eth_hdr(skb))) { 371 | if ((mdst && mdst->mglist) || 372 | br_multicast_is_router(br)) 373 | skb2 = skb; 374 | br_multicast_forward(mdst, skb, skb2); 375 | skb = NULL; 376 | if (!skb2) 377 | goto out; 378 | } else 379 | skb2 = skb; 380 | 381 | unicast = false; 382 | br->dev->stats.multicast++; 383 | } else if ((dst = __br_fdb_get(br, dest, vid)) && 384 | dst->is_local) { 385 | skb2 = skb; 386 | /* Do not forward the packet since it's local. */ 387 | skb = NULL; 388 | } 389 | 390 | ``` 391 | 392 | 这里会有一个arp代理,在本书前面关于DVR的逻辑中有提及其作用。其余的代码会判断是不是广播或多播报文,是的话就多播。继续看代码: 393 | 394 | ``` 395 | if (skb) { 396 | if (dst) { 397 | dst->used = jiffies; 398 | br_forward(dst->dst, skb, skb2); 399 | } else 400 | br_flood_forward(br, skb, skb2, unicast); 401 | } 402 | 403 | if (skb2) 404 | return br_pass_frame_up(skb2); 405 | 406 | out: 407 | return 0; 408 | drop: 409 | kfree_skb(skb); 410 | goto out; 411 | } 412 | EXPORT_SYMBOL_GPL(br_handle_frame_finish); 413 | ``` 414 | 415 | 这里的逻辑是,如果dst = __br_fdb_get(br, dest, vid)) && dst->is_local,也就是如果这个包是本地的则走br_pass_frame_up,否则走br_forward/br_flood_forward。 416 | 417 | 来看下br_forward,按照我们的分析,这个应该调用目的口的发送方法: 418 | 419 | ``` 420 | /* called with rcu_read_lock */ 421 | void br_forward(const struct net_bridge_port *to, struct sk_buff *skb, struct sk_buff *skb0) 422 | { 423 | if (should_deliver(to, skb)) { 424 | if (skb0) 425 | deliver_clone(to, skb, __br_forward); 426 | else 427 | __br_forward(to, skb); 428 | return; 429 | } 430 | 431 | if (!skb0) 432 | kfree_skb(skb); 433 | } 434 | ``` 435 | 436 | __br_forward的实现为: 437 | 438 | ``` 439 | static void __br_forward(const struct net_bridge_port *to, struct sk_buff *skb) 440 | { 441 | struct net_device *indev; 442 | 443 | if (skb_warn_if_lro(skb)) { 444 | kfree_skb(skb); 445 | return; 446 | } 447 | 448 | skb = br_handle_vlan(to->br, nbp_get_vlan_info(to), skb); 449 | if (!skb) 450 | return; 451 | 452 | indev = skb->dev; 453 | skb->dev = to->dev; 454 | skb_forward_csum(skb); 455 | 456 | NF_HOOK(NFPROTO_BRIDGE, NF_BR_FORWARD, NULL, skb, 457 | indev, skb->dev, 458 | br_forward_finish); 459 | } 460 | ...... 461 | int br_forward_finish(struct sock *sk, struct sk_buff *skb) 462 | { 463 | return NF_HOOK(NFPROTO_BRIDGE, NF_BR_POST_ROUTING, sk, skb, 464 | NULL, skb->dev, 465 | br_dev_queue_push_xmit); 466 | 467 | } 468 | ...... 469 | int br_dev_queue_push_xmit(struct sock *sk, struct sk_buff *skb) 470 | { 471 | if (!is_skb_forwardable(skb->dev, skb)) { 472 | kfree_skb(skb); 473 | } else { 474 | skb_push(skb, ETH_HLEN); 475 | br_drop_fake_rtable(skb); 476 | dev_queue_xmit(skb); 477 | } 478 | 479 | return 0; 480 | } 481 | 482 | ``` 483 | 484 | 可以看到,这里最终调用了dev_queue_xmit将包发送出去。注意:这一连串操作都是在同一个核上做的,这个对以后的性能评估会有影响。 485 | 486 | 再来看下br_pass_frame_up,按照我们的分析,这个应该调用目的口的接收方法: 487 | 488 | ``` 489 | static int br_pass_frame_up(struct sk_buff *skb) 490 | { 491 | struct net_device *indev, *brdev = BR_INPUT_SKB_CB(skb)->brdev; 492 | struct net_bridge *br = netdev_priv(brdev); 493 | struct pcpu_sw_netstats *brstats = this_cpu_ptr(br->stats); 494 | struct net_port_vlans *pv; 495 | 496 | u64_stats_update_begin(&brstats->syncp); 497 | brstats->rx_packets++; 498 | brstats->rx_bytes += skb->len; 499 | u64_stats_update_end(&brstats->syncp); 500 | 501 | /* Bridge is just like any other port. Make sure the 502 | * packet is allowed except in promisc modue when someone 503 | * may be running packet capture. 504 | */ 505 | pv = br_get_vlan_info(br); 506 | if (!(brdev->flags & IFF_PROMISC) && 507 | !br_allowed_egress(br, pv, skb)) { 508 | kfree_skb(skb); 509 | return NET_RX_DROP; 510 | } 511 | 512 | indev = skb->dev; 513 | skb->dev = brdev; 514 | skb = br_handle_vlan(br, pv, skb); 515 | if (!skb) 516 | return NET_RX_DROP; 517 | 518 | return NF_HOOK(NFPROTO_BRIDGE, NF_BR_LOCAL_IN, NULL, skb, 519 | indev, NULL, 520 | netif_receive_skb_sk); 521 | } 522 | ``` 523 | 524 | 最后的netif_receive_skb_sk就是我们的收包逻辑了,那么这里怎么保证这个数据包不会在走bridging呢?根据我们上面的分析,主要就是看sk_buff->dev->rx_handler是不是空。这里我们看到skb->dev = brdev,也就是我们的bridge。bridge的rx_handler是空的,所以接下来就走ip等上层协议了。注意,这里的一连串事情也都是发生在同一个核上的。另外netif_receive_skb_sk是poll调用软中断的软中断内部的实现,所以这个方法会等待其执行完成后才会返回,而不是走软中断这一套。 525 | 526 | 对于bridge来说基本实现就是这样。一个有趣的地方是在Nova中,如果大家观察其在物理机上tap设备的mac地址的时候,会发现这个mac地址和虚拟机的不一样,主要是第一个字节不一样: 527 | 528 | ``` 529 | #物理tap 530 | 19: tap1cb8937b-6b: mtu 1500 qdisc htb master qbr1cb8937b-6b state UNKNOWN mode DEFAULT qlen 500 531 | link/ether fe:16:3e:3f:6b:d3 brd ff:ff:ff:ff:ff:ff 532 | #虚拟机 533 | [stack@udevstack01 neutron]$ neutron port-list | grep 1cb8937b-6b 534 | | 1cb8937b-6bae-4e8c-bbdc-cdcdfb87731b | nic-1cb8937b | fa:16:3e:3f:6b:d3 | {"subnet_id": "4e43c6cf-a75f-461d-b8e7-d3bad007f251", "ip_address": "10.0.0.6"} | 535 | ``` 536 | 537 | 可以看到这里的mac地址在虚拟机中为fa:16:3e:3f:6b:d3,在物理机中为fe:16:3e:3f:6b:d3。为什么会这样呢?原因是因为linux中的bridge中的mac地址是会动态改变的,其mac地址等于其所有port中mac最小的那个port的mac地址。在libvirt的世界,当一个虚拟机生成后其mac地址是随机生成的,如果这个主机加到了bridge上并且其mac地址小于bridge中已有的所有port的mac地址,那么bridge的mac地址就会变成这个port的mac。在很多时候这种动态mac改变是会产生问题的:比如此时一个数据包可能依赖老的arp信息发给bridge,但是bridge就收不到了。 538 | 539 | 为了避免这种情况,libvirt的代码中会的把虚拟机随机生成的tap设备的mac地址的第一个字节改为fe,所以对于通常的情况:一个bridge下接了一个物理网卡和多个虚拟tap设备的情况,此时物理网卡的mac地址肯定是最小的(因为其余的虚拟tap都是fe),所以此时无论新plug到bridge上的port的mac地址是啥,其都不会小于物理网卡的地址,因此bridge的地址就不会变了。在virnetdevtap.c中有如下代码: 540 | 541 | ``` 542 | tapmac.addr[0] = 0xFE; /* Discourage bridge from using TAP dev MAC */ 543 | ``` 544 | 545 | 546 | 547 | -------------------------------------------------------------------------------- /book/ch07/s7.0.md: -------------------------------------------------------------------------------- 1 | # 容器中的网络 2 | 3 | 容器是目前比较热门的一个技术,这一章的目的就是介绍下容器中的网络。在介绍容器中的网络之前会给大家介绍一下容器的代表:Docker的基本使用方法以及其实现原理,同时本章也会介绍下Kubernetes这个容器管理集群软件的基本知识。最后会介绍一下Docker中的网络组件libnetwork的用法及实现原理。另外在下一小节笔者会说一下笔者对于容器的一些思考。 4 | 5 | 本章中的代码主要是基于GO语言的,但只要对任何一门语言有基础,则对于本章所列出的代码都不会存在理解上的问题。 6 | 7 | -------------------------------------------------------------------------------- /book/ch07/s7.1.md: -------------------------------------------------------------------------------- 1 | ## 容器 or 传统虚拟化? 2 | 3 | 在写作本书的时候,容器的火热程度已经和OpenStack不相上下了。公司中的一些潜在的客户在咨询的时候,会希望我们提供基于容器的解决方案来作为其内部的私有云解决方案。下面笔者会说一下笔者个人的看法。 4 | 5 | 笔者之前在一家国内互联网巨头公司工作,那时所在部门遇到了一个问题:机器的使用率不高。打个比方,如果一台主机的生命周期是5年,可能这台主机在其过保前其CPU使用率都始终是在一个非常非常低的水平,比如CPU平均使用率为百分之十(这个数字以及接下来提到的所有数字都不是真实数据,这里只是举这个数字方便我们下面进行计算)。可能很多人认为机器使用率低是好事,说明程序优化的好,但实际上对于研发团队来说降低代码的资源占用率当然是好事,但是对于采购部门或者公司的预算来说就不是好事情了。毕竟资源闲着就说明多花了冤枉钱,假设一共有十万台主机(这个量级对于国内很多互联网公司都是低估的),那么百分之九十的资源浪费就相当于浪费了九万台主机,每台主机算它采购价十万,这样就浪费了九个亿。所以当时笔者所在的部门的一个任务就是提高主机的资源使用率。如何提高呢?经过分析,笔者所在部门的应用是内存消耗型的,所以打算和一些CPU消耗型的应用混部。但是混部的时候要保证这些应用不能互相影响(当然混部还有非常多的要考虑的地方,比如优先级等等,这里我们只讲本章关注的),比如CPU消耗型应用A所占用的内存规定是多少就只能是多少,否则会影响到同一台物理机上的内存消耗型业务。为了解决这个问题当时的做法是基于CGroup。本章下面介绍Docker的时候大家会看到CGroup是Docker的核心技术。可能有人会问通过虚拟化不是也能做到这一点吗?确实虚拟化也能实现这一点,但是虚拟化存在下面的一些问题: 6 | 7 | 1. 虚拟化本身消耗的资源过多,我们的最根本目的是为了省钱,所以相比之下虚拟化不是最好的选择 8 | 2. 我们的应用会的被频繁的启动、停止、更新。基于虚拟化的话启动、停止等操作速度太慢 9 | 3. 虚拟化提供了一个完全隔离的环境,这个是虚拟化的优点,但是对于我们这个完全是内部使用的环境来说这不是我们最需要的功能 10 | 11 | 因此我们选用了CGroup,大家可以看成是使用了容器来实现混部这么一个需求。对于这套系统的开发者来说其主要的工作量是用于控制系统的开发,比如我启动了一个应用A,这个应用A如何在集群中找到合适的主机启动,同时如何同步相关联监控、元数据管理等等。这一点上笔者想说的是:对于容器或虚拟化来说,控制系统的设计存在细微差别,但核心思想是一样的。 12 | 13 | 这就是笔者所切身经历的一个基于容器技术的一个例子,大家可以看到使用容器比使用虚拟化所带来的优点。但笔者必须指出一些现实的问题: 14 | 15 | 1. 当时笔者所在的公司规模很大,非常大。集群上跑的业务只有超大规模的互联网公司才会接触到。如果使用虚拟化比使用容器会多消耗百分之十到百分之三十的资源,对于当时笔者所在的公司确实是一笔很客观的数目,但是对于一般企业来说这个数目是否值得去做需要大家考虑 16 | 2. 虽然CGroup做了隔离,但这种隔离并不完全。不同部门在运行这些业务之前需要仔细划定业务优先级等信息(用云的术语来说就是超卖)。CGroup中内存方面的超卖一不小心就是OOM 17 | 3. 当时我们在这套系统上运行的业务系统和传统公司所运行的业务系统不太一样。举个可能夸张一些的例子,传统公司会习惯于在一台主机上运行LAMP(Linux+Apache+MySQL+PHP)来支持其业务,但如果要放到我们上面的系统里,就需要启动两个任务分别运行Apache+PHP、MySQL。同时其需要花时间、精力在上述系统的控制器的实现上 18 | 4. 考虑一下业务的长期发展。对于笔者当时所在的公司,底层服务已经能做到很好的scale-out了。其底层服务可以很好的应付宕机、突然假死等问题。比方说节点A宕机了,那么就丢弃节点A即可,业务不会受到影响。在这种情况下使用集群+容器的方法进行资源的调度是非常合适的。但是对于传统企业来说要做到这一点还需要时间,甚至可能其在接下来很多年里其业务量都达不到这个水平 19 | 5. 如果一个企业需要的是一个虚拟机,那么就不适合使用容器了。容器更倾向于PaaS而不是IaaS。大家一定要清楚这两者的区别,你买了PaaS的MySQL服务后对提供商说你想在上面再跑个Apache,提供商一定会让你再去买一个Apache容器。但如果你买了个IaaS,你想在上面干啥就能干啥,只不过安装部署等需要你自己完成。对于集中式的云服务提供商来说提供基于容器的PaaS是一个不错的选择,但对于传统企业来说就需要考虑考虑了。 20 | 21 | 所以笔者个人认为,对于容器技术比较适合在一些超大型公司使用。对于这些公司来说运行在容器集群之上的业务已经能做到宕机一台不担心,宕机十台也不担心的地步。或者当一些公司希望提供PaaS平台给外界使用的时候,基于容器也是一个不错的选择,但这和之前说的内部业务还存在区别,如果是提供基于容器的PaaS,则控制器必须考虑运行在容器上的客户业务的高可用、资源扩展等等,比如对于大型互联网公司来说,一个运行在容器上的MySQL挂了就挂了,没关系,但是对于来买你服务的客户来说他可不希望买到的一个MySQL容器没有任何的高可用。同时就拿后者来说,如果是MySQL,还得考虑MySQL的备份、性能诊断等等方面,从这个角度来说,如果要对外提供一个PaaS平台,或许更多的考虑并不在于是基于容器还是虚拟化,而是一些其它部分吧。慢慢的我们会看到一些软件其本身就是基于提供PaaS服务而设计的,笔者认为这也是一种方向。 22 | 23 | 所以笔者个人认为,目前容器还是超大规模公司特定业务的工具。 24 | 25 | 另外还有很多的文章是关于基于容器实现开发、测试、部署一条龙服务的。关于这一点笔者并没有实际的经验,如果容器真的能解决这个问题当然是太赞了,因为笔者目前所看到的一条龙都是少量自动化混杂着大量人肉的自动化,有些甚至还做的不如原来手工部署来的方便。但我想这个对于传统企业(很多企业是没有开发部门的)来说应该不是一个要考虑的事情。 26 | 27 | 希望写到这里,大家可以自己更多的思考下应该在实际的环境中使用容器还是使用虚拟化技术。 28 | -------------------------------------------------------------------------------- /book/ch07/s7.2.md: -------------------------------------------------------------------------------- 1 | ## Docker 2 | 3 | 容器技术中,目前最有代表性的就是Docker了。本书也会以Docker为例,讲一下容器中的网络,因此我们需要熟悉一下Docker。这一节我们会讲一下Docker的基本用法、实现原理以及其代码设计层面的软件架构。 4 | 5 | 6 | ### Docker基本用法 7 | 8 | 本书不是一本关于Docker的书,所以不会给出非常详细的用法说明。对于初学者来说可以读下Docker的官方文档或者《Orchestrating Docker》一书来掌握其用法。 9 | 10 | 下面来看一个基本用法的例子,对于例子中所讲的daemon、容器、镜像等术语我们会在下一节进行说明。我们的例子是需要启动一个运行MySQL的容器。命令如下: 11 | 12 | ``` 13 | 启动docker daemon: 14 | [root@dev ~]# service docker restart 15 | 查看目前所拥有的镜像: 16 | [root@dev ~]# docker images 17 | REPOSITORY TAG IMAGE ID CREATED VIRTUAL SIZE 18 | libnetwork-build latest c99f5cf5eb23 12 days ago 587.5 MB 19 | docker.io/golang 1.4 124e2127157f 2 weeks ago 517.2 MB 20 | docker.io/berngp/docker-zabbix latest ad689c775bbf 6 weeks ago 1.134 GB 21 | docker.io/centos 7 7322fbe74aa5 6 weeks ago 172.2 MB 22 | docker.io/centos latest 7322fbe74aa5 6 weeks ago 172.2 MB 23 | 下载一个MySQL镜像,其源托管在https://registry.hub.docker.com/_/mysql/上: 24 | [root@dev ~]# docker pull mysql 25 | latest: Pulling from docker.io/mysql 26 | 4c8cbfd2973e: Downloading [=> ] 1.129 MB/37.21 MB 27 | 60c52dbe9d91: Download complete 28 | 4c8cbfd2973e: Pull complete 29 | 60c52dbe9d91: Pull complete 30 | c2b0136be90f: Pull complete 31 | 273cd71eacf0: Pull complete 32 | 543ff72402d8: Pull complete 33 | aa3022270c68: Pull complete 34 | 39130042665d: Pull complete 35 | 2e4d19227c16: Pull complete 36 | 1f877cc70688: Pull complete 37 | 7e6d170eec04: Pull complete 38 | 07264b223269: Pull complete 39 | b95dfc449f80: Pull complete 40 | 45d84ed3b24d: Pull complete 41 | bcf7334ef42a: Pull complete 42 | a128139aadf2: Already exists 43 | docker.io/mysql:latest: The image you are pulling has been verified. Important: image verification is a tech preview feature and should not be relied on to provide security. 44 | Digest: sha256:3e633be4546d5549c35f9731280e7c7ef0a272c23dfbe241e08a655920c2ffa1 45 | Status: Downloaded newer image for docker.io/mysql:latest 46 | 从上面给出的连接中可以看出这个镜像提供了一些参数可以在启动的时候使用,包括:MYSQL_ROOT_PASSWORD、MYSQL_DATABASE、MYSQL_USER、MYSQL_PASSWORD等等。这里我们使用MYSQL_DATABASE、MYSQL_USER、MYSQL_PASSWORD这三个操作来让我们的镜像启动的时候自动建立一个数据库并建立有权限操作该数据库的相关用户。同时我们映射启动后容器中的3306端口对应物理机的6033端口: 47 | [root@dev ~]# docker run --name NeutronMySQL -p 6033:3306 -e MYSQL_USER=neutron -e MYSQL_PASSWORD=password -e MYSQL_DATABASE=neutron -e MYSQL_ROOT_PASSWORD=password docker.io/mysql 48 | Running mysql_install_db 49 | 2015-08-02 15:06:58 0 [Note] /usr/sbin/mysqld (mysqld 5.6.26) starting as process 33 ... 50 | 2015-08-02 15:06:58 33 [Note] InnoDB: Using atomics to ref count buffer pool pages 51 | 2015-08-02 15:06:58 33 [Note] InnoDB: The InnoDB memory heap is disabled 52 | 2015-08-02 15:06:58 33 [Note] InnoDB: Mutexes and rw_locks use GCC atomic builtins 53 | 2015-08-02 15:06:58 33 [Note] InnoDB: Memory barrier is not used 54 | ...... 55 | 现在我们在物理机上连接一下我们的neutron数据库试试: 56 | [root@dev ~]# mysql -uneutron -ppassword -P6033 -h127.0.0.1 neutron 57 | Welcome to the MariaDB monitor. Commands end with ; or \g. 58 | Your MySQL connection id is 2 59 | Server version: 5.6.26 MySQL Community Server (GPL) 60 | 61 | Copyright (c) 2000, 2014, Oracle, MariaDB Corporation Ab and others. 62 | 63 | Type 'help;' or '\h' for help. Type '\c' to clear the current input statement. 64 | 65 | MySQL [neutron]> 66 | ``` 67 | 可以看到通过docker run这么一个命令我们就运行了一个完整的数据库,并且这个运行的时候可以指定某些参数的值,这一点如果要通过虚拟机来实现那么是比较麻烦的。 68 | 69 | 70 | ### Docker实现原理 71 | 72 | 上面看过一个例子后,这里我们来讲一下Docker的实现原理。Docker的实现依赖两个东西: 73 | 74 | 1. namespace 75 | 2. CGroup 76 | 77 | namespace在前面我们讲过,主要用于提供名字空间的隔离。我们之前分析过网络namespace的实现,因此这里就不多介绍了。CGroup的作用大家可以参考这个连接的介绍: 78 | 79 | * http://coolshell.cn/articles/17049.html 80 | 81 | 简单的说,CGroup的作用就是限制某个进程可以使用的系统资源。比如限制一个进程只可以使用1个G的内存,或者限制这个进程的IO速率等等。 82 | 83 | 当我们运行docker XXX命令的时候,大部分命令发送给了一个叫做daemon的进程。这个daemon进程的启动、停止方式也是通过docker命令完成的: 84 | 85 | ``` 86 | # 启动daemon 87 | [root@dev ~]# docker -d 88 | ``` 89 | 90 | 接下来当我们执行如docker ps这种查看当前有哪些正在运行的容器的命令的时候,docker ps会的转成一个满足RESTful要求的HTTP请求,然后这个HTTP请求会通过TCP或者本地套接字的方式发送给我们的daemon进程。daemon守护进程进行相关操作后会返回结果给我们运行docker ps的客户端,接着docker ps会的输出结果。也就是说docker其实就是在我们的本机启动了一个server,然后docker ps这类命令就是和我们的server进行普通的交互,且这种交互是基于HTTP的请求。 91 | 92 | 现在来解释下什么是容器。在上面的MySQL例子执行后,我们在系统中可以看到如下进程: 93 | 94 | ``` 95 | [root@dev ~]# ps -elf | grep NeutronMySQL 96 | 4 S root 2535 2204 0 80 0 - 51809 ep_pol 23:06 pts/0 00:00:00 docker run --name NeutronMySQL -p 6033:3306 -e MYSQL_USER=neutron -e MYSQL_PASSWORD=password -e MYSQL_DATABASE=neutron -e MYSQL_ROOT_PASSWORD=password docker.io/mysql 97 | ``` 98 | 99 | 对于Docker来说,一个容器其实就是一个进程。这个进程运行在自己独立的namespace下面,所以这个pid为2535进程其看到的namespace都是独立的。当我们执行docker run命令后,docker首先会启动一个进程,然后设置这个进程使用新的namespace(如果对namespace不熟悉的话,建议去第五章复习下namespace的相关知识)。接着会在这个namespace中运行预定义的一些指令,比如启动一个MySQL进程,然后建立一个数据库等等。可以看到这个进程之所以能和我们的物理机隔离的关键正是在于namespace。那么CGroup有什么用呢?我们的例子里并没有限制我们的容器(也就是我们的进程)能使用多少系统资源,加入我希望我的MySQL容器只能使用10个G的系统内存,那么这里可以通过CGroup实现资源限制。 100 | 101 | 接着我们来解释下镜像。上面说了容器其实就是一个进程,生活在自己独立的namespace中。那么这个容器进程所看到的文件系统是什么呢?这个容器进程所看到的文件系统是DOcker通过chroot来实现的。chroot是Linux很早就提供的一个命令,用于修改某个进程所看到的根文件系统路径。比如在进程A的执行代码中执行类似chroot /tmp/a的命令,则在此之后A执行ls /命令看到的就是/tmp/a下面的内容了。因此在Docker中每个容器都是通过chroot命令获取自己独立的文件系统的。但是如果每次启动容器都需要安装MySQL等等才能使用的话会很麻烦,所以有人会事先建立好类似/tmp/a之类的目录,在这个目录下会建立如/tmp/a/bin/mysqld、/tmp/a/etc/my.cnf之类的文件并进行配置,接着容器启动的时候设置/tmp/a为根目录后,其就能执行mysqld命令,并且获取到预先设置的my.cnf配置文件了。这样的一个/tmp/a所打包生成的一个文件其实就是一个镜像的雏形。那么实际上的镜像和这个雏形有什么区别呢?如果按照我们刚刚说的,每个容器启动后都需要chroot自己的根文件系统,则如果有100个容器则会有100个类似于/tmp/a的目录存在,这样对于资源的利用以及容器的启动速度(也就是这个进程的初始化速度)是很不利的。因为类似于mysqld这样的文件大家都是公用的,完全没有必要在/tmp/a/bin下有一份,同时在/tmp/b/bin下也有一份。为了解决这个问题Docker使用了支持多层次的文件系统,比如AUFS或者Device Mapper。他们的作用简单的说就是对于容器A,启动的时候系统不会从/tmp/mysql复制一个完整的目录到/tmp/a下,而是建立一个空的/tmp/A。然后通过AUFS或Device Mapper的技术挂载出一个/tmp/a目录,对该目录的所有读操作会先在/tmp/A下进行,如果/tmp/A下没有找到对应的文件则会去/tmp/mysql下读取。对于该目录的所有写操作则都只会发生在/tmp/A。比如用户修改了my.cnf,则实际上的操作是从/tmp/mysql/etc/my.cnf复制一份到/tmp/A/etc/my.cnf,然后再对/tmp/A/etc/my.cnf进行修改。Docker在启动容器的时候,会先设置好这些,然后再chroot /tmp/a为根目录。可以看到通过这种多层次的文件系统用户可以对底层的/tmp/mysql进行定制后再提供服务给其他人使用,比如用户在进行上面的操作后,安装了一个Apache+PHP在容器中,此时我们的/tmp/mysql + /tmp/A合并而成的/tmp/a就能提供一个完整的LAMP服务了。于是可以将/tmp/mysql + /tmp/A一起打包成一个文件提供出去。当某个用户需要一个LAMP的容器B在上面运行一个wordpress应用的时候,其解压这个文件,然后Docker建立空的/tmp/B,并通过上面说的AUFS或Device Mapper将/tmp/mysql、/tmp/A、/tmp/B挂载为/tmp/b,然后启动的新容器chroot到/tmp/b后就能直接使用LAMP环境了。这里说到的/tmp/mysql加上/tmp/A以及一些元信息所共同打包二层的一个文件就是我们所说的镜像。可以看到镜像是分层的。 102 | 103 | 以上这些就是Docker的实现原理,说实话从原理上看真的很简单,因为Docker使用的技术都是一些现有的技术。 104 | 105 | 106 | ### Docker代码实现导读 107 | 108 | 由于读者在看完本章后可能会去深入学习libnetwork的代码,所以笔者这里简单的说下Docker的代码如何去阅读,帮助大家快速的定位libnetwork在Docker代码中的地位及使用场合。 109 | 110 | 再讲Docker代码之前,先给大家说下GO语言运行或编译时候的一些机制,笔者在学习Docker的时候就因为GO的基本功不扎实所以看漏了很多东西。对于GO不熟悉的读者可以跳过这一部分。 111 | 112 | 1. packege的init函数会的在main函数之前运行。Docker在代码中实现了很多的钩子函数,很多模块会的注册自己的实现到这些钩子上。这些注册的时机一般都发生在package的init函数中。所以看代码的时候如果发现有个地方调用预先注册的钩子,那么在查看实现的时候就需要去init函数中看下是不是在init中注册的钩子。 113 | 2. "./..."。GO中的...具有特殊的含义,表示嵌套递归所有的子目录及子目录下的子目录中的文件。大家在看Makefile的时候如果看到...就知道这个命令是作用于该文件夹下面的所有文件的。 114 | 3. "+build"。在一些文件的最开头可以看到类似"// +build libnetwork_discovery"这样的代码,这类代码大家理解为影响编译器的宏即可。比如"go build -tags libnetwork_discovery ./..."则会在发现相同实现的时候,选择拥有"// +build libnetwork_discovery"的代码进行编译。 115 | 116 | 我们来看下Docker的代码。看代码要从Makefile开始看起,这里Makefile笔者就不多说了,Docker的代码有个比较好玩的地方在于其编译、连接及测试都是跑在一个容器中的,Makefile中给出了其实现。 117 | 118 | 对于docker来说,入口函数为main,main中笔者想分析的代码为: 119 | 120 | ``` 121 | if *flDaemon { 122 | if *flHelp { 123 | flag.Usage() 124 | return 125 | } 126 | mainDaemon() 127 | return 128 | } 129 | 130 | // From here on, we assume we're a client, not a server. 131 | ``` 132 | 133 | 如果docker命令启动的时候加了-D,则这里的flDaemon这个flag就是True,于是会执行mainDaemon()启动我们的docker server。我们先分析client的。一个client在docker中为: 134 | 135 | ``` 136 | cli := client.NewDockerCli(stdin, stdout, stderr, *flTrustKey, protoAddrParts[0], protoAddrParts[1], tlsConfig) 137 | 138 | if err := cli.Cmd(flag.Args()...); err != nil { 139 | if sterr, ok := err.(client.StatusError); ok { 140 | if sterr.Status != "" { 141 | fmt.Fprintln(cli.Err(), sterr.Status) 142 | os.Exit(1) 143 | } 144 | os.Exit(sterr.StatusCode) 145 | } 146 | fmt.Fprintln(cli.Err(), err) 147 | os.Exit(1) 148 | } 149 | 150 | ...... 151 | 152 | // The transport is created here for reuse during the client session. 153 | tr := &http.Transport{ 154 | TLSClientConfig: tlsConfig, 155 | } 156 | sockets.ConfigureTCPTransport(tr, proto, addr) 157 | 158 | configFile, e := cliconfig.Load(cliconfig.ConfigDir()) 159 | if e != nil { 160 | fmt.Fprintf(err, "WARNING: Error loading config file:%v\n", e) 161 | } 162 | 163 | return &DockerCli{ 164 | proto: proto, 165 | addr: addr, 166 | configFile: configFile, 167 | in: in, 168 | out: out, 169 | err: err, 170 | keyFile: keyFile, 171 | inFd: inFd, 172 | outFd: outFd, 173 | isTerminalIn: isTerminalIn, 174 | isTerminalOut: isTerminalOut, 175 | tlsConfig: tlsConfig, 176 | scheme: scheme, 177 | transport: tr, 178 | } 179 | ``` 180 | 181 | 当docker执行client的命令的时候,会通过GO提供的HTTP Client发送请求给docker server。什么时候调用HTTP Client我们下面会看到,先来看下cli.Cmd的实现: 182 | 183 | ``` 184 | // Cmd executes the specified command. 185 | func (cli *DockerCli) Cmd(args ...string) error { 186 | if len(args) > 1 { 187 | method, exists := cli.getMethod(args[:2]...) 188 | if exists { 189 | return method(args[2:]...) 190 | } 191 | } 192 | if len(args) > 0 { 193 | method, exists := cli.getMethod(args[0]) 194 | if !exists { 195 | return fmt.Errorf("docker: '%s' is not a docker command.\nSee 'docker --help'.", args[0]) 196 | } 197 | return method(args[1:]...) 198 | } 199 | return cli.CmdHelp() 200 | } 201 | ...... 202 | func (cli *DockerCli) getMethod(args ...string) (func(...string) error, bool) { 203 | camelArgs := make([]string, len(args)) 204 | for i, s := range args { 205 | if len(s) == 0 { 206 | return nil, false 207 | } 208 | camelArgs[i] = strings.ToUpper(s[:1]) + strings.ToLower(s[1:]) 209 | } 210 | methodName := "Cmd" + strings.Join(camelArgs, "") 211 | method := reflect.ValueOf(cli).MethodByName(methodName) 212 | if !method.IsValid() { 213 | return nil, false 214 | } 215 | return method.Interface().(func(...string) error), true 216 | } 217 | 218 | ``` 219 | 220 | 这里通过反射的方法获取了实际的命令。类似于Python中的getattr方法。在client的目录下我们可以看到这些Cmd打头的函数: 221 | 222 | ``` 223 | Cmd 224 | CmdAttach 225 | CmdBuild 226 | CmdCommit 227 | CmdCp 228 | CmdCreate 229 | CmdDiff 230 | CmdEvents 231 | CmdExec 232 | CmdExport 233 | CmdHelp 234 | CmdHistory 235 | CmdImages 236 | CmdImport 237 | CmdInfo 238 | CmdInspect 239 | CmdKill 240 | CmdLoad 241 | CmdLogin 242 | CmdLogout 243 | CmdLogs 244 | CmdNetwork 245 | CmdPause 246 | CmdPort 247 | CmdPs 248 | CmdPull 249 | CmdPush 250 | CmdRename 251 | CmdRestart 252 | CmdRm 253 | CmdRmi 254 | CmdRun 255 | CmdSave 256 | CmdSearch 257 | CmdService 258 | CmdStart 259 | CmdStats 260 | CmdStop 261 | CmdTag 262 | CmdTop 263 | CmdUnpause 264 | CmdVersion 265 | CmdWait 266 | ``` 267 | 268 | 因此我们要分析一个Client请求如何实现的时候我们只要这里选取一个分析即可。我们来看下CmdPs的实现,其重要的代码为: 269 | 270 | ``` 271 | rdr, _, _, err := cli.call("GET", "/containers/json?"+v.Encode(), nil, nil) 272 | if err != nil { 273 | return err 274 | } 275 | 276 | defer rdr.Close() 277 | 278 | containers := []types.Container{} 279 | if err := json.NewDecoder(rdr).Decode(&containers); err != nil { 280 | return err 281 | } 282 | 283 | w := tabwriter.NewWriter(cli.out, 20, 1, 3, ' ', 0) 284 | if !*quiet { 285 | fmt.Fprint(w, "CONTAINER ID\tIMAGE\tCOMMAND\tCREATED\tSTATUS\tPORTS\tNAMES") 286 | 287 | if *size { 288 | fmt.Fprintln(w, "\tSIZE") 289 | } else { 290 | fmt.Fprint(w, "\n") 291 | } 292 | } 293 | ``` 294 | 295 | 可以看到docker将我们的请求转为一个对HTTP URL的请求。call方法是怎么实现的呢?关联代码如下: 296 | 297 | ``` 298 | func (cli *DockerCli) call(method, path string, data interface{}, headers map[string][]string) (io.ReadCloser, http.Header, int, error) { 299 | params, err := cli.encodeData(data) 300 | if err != nil { 301 | return nil, nil, -1, err 302 | } 303 | 304 | if data != nil { 305 | if headers == nil { 306 | headers = make(map[string][]string) 307 | } 308 | headers["Content-Type"] = []string{"application/json"} 309 | } 310 | 311 | serverResp, err := cli.clientRequest(method, path, params, headers) 312 | return serverResp.body, serverResp.header, serverResp.statusCode, err 313 | } 314 | 315 | ...... 316 | 317 | func (cli *DockerCli) clientRequest(method, path string, in io.Reader, headers map[string][]string) (*serverResponse, error) { 318 | 319 | serverResp := &serverResponse{ 320 | body: nil, 321 | statusCode: -1, 322 | } 323 | ...... 324 | req.Header.Set("User-Agent", "Docker-Client/"+dockerversion.VERSION+" ("+runtime.GOOS+")") 325 | req.URL.Host = cli.addr 326 | req.URL.Scheme = cli.scheme 327 | ...... 328 | if expectedPayload && req.Header.Get("Content-Type") == "" { 329 | req.Header.Set("Content-Type", "text/plain") 330 | } 331 | 332 | resp, err := cli.HTTPClient().Do(req) 333 | ...... 334 | 335 | ...... 336 | 337 | // HTTPClient creates a new HTTP client with the cli's client transport instance. 338 | func (cli *DockerCli) HTTPClient() *http.Client { 339 | return &http.Client{Transport: cli.transport} 340 | ``` 341 | 342 | 可以看到具体的请求最后会通过http这个packege的Client来实现。 343 | 344 | Client端的代码就是这么一个框架,其他的Cmd命令基本上也是这么一个模式。我们接下来看下docker server端的代码。按照上面分析的,我们从mainDaemon开始看,重要的代码为: 345 | 346 | ``` 347 | serverConfig := &apiserver.ServerConfig{ 348 | Logging: true, 349 | EnableCors: daemonCfg.EnableCors, 350 | CorsHeaders: daemonCfg.CorsHeaders, 351 | Version: dockerversion.VERSION, 352 | } 353 | serverConfig = setPlatformServerConfig(serverConfig, daemonCfg) 354 | ...... 355 | api := apiserver.New(serverConfig) 356 | 357 | ...... 358 | 359 | registryService := registry.NewService(registryCfg) 360 | d, err := daemon.NewDaemon(daemonCfg, registryService) 361 | if err != nil { 362 | if pfile != nil { 363 | if err := pfile.Remove(); err != nil { 364 | logrus.Error(err) 365 | } 366 | } 367 | logrus.Fatalf("Error starting daemon: %v", err) 368 | } 369 | 370 | ...... 371 | 372 | // after the daemon is done setting up we can tell the api to start 373 | // accepting connections with specified daemon 374 | api.AcceptConnections(d) 375 | ``` 376 | 377 | 这里启动了一个apiserver。我们可以认为这个apiserver就是提供HTTP服务的server。同时这里还建立了一个daemon。这里来说下apiserver和daemon的关系。其实这个关系很简单,就是一个MVC。apiserver负责监听HTTP请求,然后分析这个请求要交由那个后端进行处理(一般我们把这个后端称为controller,而apiserver则称为view)。后端在实现代码的时候可能会需要操作AUFS或者Device Mapper,但是我们知道AUFS是Ubuntu支持的,而Ubuntu默认不支持Device Mapper,所以后端如何区别这种事情呢?后端当然不想管啦(因为要降低耦合),所以后端会调用daemon来操作。daemon会暴露出公共的接口给后端使用,后端调用daemon的接口,然后daemon的接口再去调用真正的driver来干活。这里的driver也是在启动的时候根据配置即参数初始化完成的。所以对于daemon来说笔者更加倾向于叫他model。 378 | 379 | 我们先来看下apiserver的初始化。切入口为api := apiserver.New(serverConfig),核心代码: 380 | 381 | ``` 382 | func New(cfg *ServerConfig) *Server { 383 | srv := &Server{ 384 | cfg: cfg, 385 | start: make(chan struct{}), 386 | } 387 | r := createRouter(srv) 388 | srv.router = r 389 | return srv 390 | } 391 | ``` 392 | 393 | 熟悉OpenStack中使用频率特别高的routes组件的应该能猜到,这里的router就是决定了一个URL最终映射到什么处理函数的一个路由。我们可以看到createRoute的代码: 394 | 395 | ``` 396 | // we keep enableCors just for legacy usage, need to be removed in the future 397 | func createRouter(s *Server) *mux.Router { 398 | r := mux.NewRouter() 399 | if os.Getenv("DEBUG") != "" { 400 | ProfilerSetup(r, "/debug/") 401 | } 402 | m := map[string]map[string]HttpApiFunc{ 403 | "GET": { 404 | "/_ping": s.ping, 405 | ...... 406 | }, 407 | "POST": { 408 | "/auth": s.postAuth, 409 | ...... 410 | }, 411 | "DELETE": { 412 | "/containers/{name:.*}": s.deleteContainers, 413 | ...... 414 | }, 415 | "OPTIONS": { 416 | "": s.optionsHandler, 417 | }, 418 | } 419 | 420 | // If "api-cors-header" is not given, but "api-enable-cors" is true, we set cors to "*" 421 | // otherwise, all head values will be passed to HTTP handler 422 | corsHeaders := s.cfg.CorsHeaders 423 | if corsHeaders == "" && s.cfg.EnableCors { 424 | corsHeaders = "*" 425 | } 426 | 427 | for method, routes := range m { 428 | for route, fct := range routes { 429 | logrus.Debugf("Registering %s, %s", method, route) 430 | // NOTE: scope issue, make sure the variables are local and won't be changed 431 | localRoute := route 432 | localFct := fct 433 | localMethod := method 434 | 435 | // build the handler function 436 | f := makeHttpHandler(s.cfg.Logging, localMethod, localRoute, localFct, corsHeaders, version.Version(s.cfg.Version)) 437 | 438 | // add the new route 439 | if localRoute == "" { 440 | r.Methods(localMethod).HandlerFunc(f) 441 | } else { 442 | r.Path("/v{version:[0-9.]+}" + localRoute).Methods(localMethod).HandlerFunc(f) 443 | r.Path(localRoute).Methods(localMethod).HandlerFunc(f) 444 | } 445 | } 446 | } 447 | 448 | return r 449 | } 450 | ``` 451 | 452 | 联系下本书之前对于routes的介绍,这里几乎就是一样的。只不过由于GO本身的问题,这里必须明确写明URL与后端处理函数的对应关系。比如"/containers/{name:.*}"这个URL必须明确指出是通过s.deleteContainers来实现其后端。 453 | 454 | 在这些URL关系都初始化后,最主要的代码就是监听HTTP请求了。这部分代码不影响我们学习libnetwork,有兴趣的读者可以自己去分析下,并不是很复杂。 455 | 456 | 在讲daemon之前我们先从代码上验证下笔者刚刚说的MVC模型。比如对于docker ps,根据上面的代码可以找到其实现为s.getContainersJSON,而后者的实现为: 457 | 458 | ``` 459 | func (s *Server) getContainersJSON(version version.Version, w http.ResponseWriter, r *http.Request, vars map[string]string) error { 460 | if err := parseForm(r); err != nil { 461 | return err 462 | } 463 | 464 | config := &daemon.ContainersConfig{ 465 | All: boolValue(r, "all"), 466 | Size: boolValue(r, "size"), 467 | Since: r.Form.Get("since"), 468 | Before: r.Form.Get("before"), 469 | Filters: r.Form.Get("filters"), 470 | } 471 | 472 | if tmpLimit := r.Form.Get("limit"); tmpLimit != "" { 473 | limit, err := strconv.Atoi(tmpLimit) 474 | if err != nil { 475 | return err 476 | } 477 | config.Limit = limit 478 | } 479 | 480 | containers, err := s.daemon.Containers(config) 481 | if err != nil { 482 | return err 483 | } 484 | 485 | return writeJSON(w, http.StatusOK, containers) 486 | } 487 | ``` 488 | 489 | 这里的关键在于s.daemon.Containers(config)这行代码,可以看到我们的后端函数s.getContainersJSON在得到获取容器列表的请求后,通过daemon查询了容器信息(这个例子比较巧,很容易能将daemon类比为数据库)。 490 | 491 | 现在我们看下一个请求的处理过程:用户敲下docker ps命令,docker将这个命令转成HTTP请求发给docker server,docker server得到这个HTTP请求后解析出对应的后端处理函数s.getContainersJSON,然后后端处理函数在调用daemon提供的接口获取或操作对应的对象。daemon屏蔽了HTTP这一层与底层实现的耦合。而daemon在启动的时候必然要初始化底层实现的各种driver,比如操作cgroup的driver,又比如是使用AUFS还是Device Mapper等等,这些事情都由daemon来完成。比如daemon.FSDriver在初始化的时候赋值了AUFSDriver,则上层在调用daemon.FSDriver.Get()的时候,daemon会使用AUFSDriver.GET()来完成请求。这就是一个请求的基本处理过程。 492 | 493 | 在上面说了daemon的作用后,笔者这里特意列下其初始化代码中的几个重要组件: 494 | 495 | ``` 496 | ...... 497 | driver, err := graphdriver.New(config.Root, config.GraphOptions) 498 | ...... 499 | d.netController, err = initNetworkController(config) 500 | ...... 501 | ed, err := execdrivers.NewDriver(config.ExecDriver, config.ExecOptions, config.ExecRoot, config.Root, sysInitPath, sysInfo) 502 | ``` 503 | 504 | 这里的graphdriver.New会根据配置文件决定是使用AUFS还是Device Mapper还是其他什么Driver。initNetworkController会初始化网络环境,并决定使用何种网络的Driver,execdrivers.NewDriver会初始化执行环境,并决定使用何种执行Driver。这些对象初始化后都会赋值给daemon。 505 | 506 | 现在的趋势是网络Driver使用libnetwork,而执行Driver使用libcontainer。libnetwork是一个GO实现的网络相关操作的package,而libnetwork则是GO实现的一个操作CGroup等资源package。这两个package都可以给其它项目使用。我们来看下initNetworkController的实现: 507 | 508 | ``` 509 | func initNetworkController(config *Config) (libnetwork.NetworkController, error) { 510 | netOptions, err := networkOptions(config) 511 | if err != nil { 512 | return nil, err 513 | } 514 | 515 | controller, err := libnetwork.New(netOptions...) 516 | if err != nil { 517 | return nil, fmt.Errorf("error obtaining controller instance: %v", err) 518 | } 519 | ``` 520 | 可以看到这里是通过调用libnetwork的相关函数来实现网络Controller的init的。这里我们第一次看到了libnetwork,并且也对其在Docker中何时会被调用有了一个基本认识。可以想象在docker的那些网络相关的命令中,最后后端代码的实现肯定是调用daemon的相关函数,而这些daemon的相关函数的实现肯定是通过libnetwork实现的。这里我们把这条路给串起来了。 521 | 522 | 523 | ### Docker中网络相关的规则 524 | 525 | 本节最后我们来看一些Docker中网络建立后的iptables及路由表规则。 526 | 527 | 在docker daemon没有启动的时候,可以发现系统上只有libvirt建立的默认网桥: 528 | ``` 529 | [root@dev ~]# systemctl status docker.service 530 | docker.service - Docker Application Container Engine 531 | Loaded: loaded (/usr/lib/systemd/system/docker.service; disabled) 532 | Active: inactive (dead) 533 | Docs: http://docs.docker.com 534 | 535 | [root@dev ~]# brctl show 536 | bridge name bridge id STP enabled interfaces 537 | virbr0 8000.52540027e8bc yes virbr0-nic 538 | ``` 539 | 540 | 同时观察对应的iptables规则,可以看到规则是很干净的: 541 | ``` 542 | [root@dev ~]# iptables-save 543 | # Generated by iptables-save v1.4.21 on Thu Aug 6 13:40:20 2015 544 | *nat 545 | :PREROUTING ACCEPT [13:997] 546 | :INPUT ACCEPT [0:0] 547 | :OUTPUT ACCEPT [0:0] 548 | :POSTROUTING ACCEPT [0:0] 549 | COMMIT 550 | # Completed on Thu Aug 6 13:40:20 2015 551 | # Generated by iptables-save v1.4.21 on Thu Aug 6 13:40:20 2015 552 | *filter 553 | :INPUT ACCEPT [0:0] 554 | :FORWARD ACCEPT [0:0] 555 | :OUTPUT ACCEPT [44:5440] 556 | -A INPUT -m state --state RELATED,ESTABLISHED -j ACCEPT 557 | -A INPUT -p icmp -j ACCEPT 558 | -A INPUT -i lo -j ACCEPT 559 | -A INPUT -p tcp -m state --state NEW -m tcp --dport 22 -j ACCEPT 560 | -A INPUT -j REJECT --reject-with icmp-host-prohibited 561 | -A FORWARD -j REJECT --reject-with icmp-host-prohibited 562 | COMMIT 563 | # Completed on Thu Aug 6 13:40:20 2015 564 | ``` 565 | 566 | 现在启动docker daemon: 567 | ``` 568 | [root@dev ~]# service docker start 569 | Redirecting to /bin/systemctl start docker.service 570 | [root@dev ~]# brctl show 571 | bridge name bridge id STP enabled interfaces 572 | docker0 8000.56847afe9799 no 573 | virbr0 8000.52540027e8bc yes virbr0-nic 574 | [root@dev ~]# ip l show dev docker0 575 | 8: docker0: mtu 1500 qdisc noqueue state DOWN mode DEFAULT 576 | link/ether 56:84:7a:fe:97:99 brd ff:ff:ff:ff:ff:ff 577 | ``` 578 | 579 | 可以发现出现了一个docker0的bridge。同时我们看下iptables规则: 580 | ``` 581 | [root@dev ~]# iptables-save 582 | # Generated by iptables-save v1.4.21 on Thu Aug 6 13:41:52 2015 583 | *nat 584 | :PREROUTING ACCEPT [22:1779] 585 | :INPUT ACCEPT [0:0] 586 | :OUTPUT ACCEPT [0:0] 587 | :POSTROUTING ACCEPT [0:0] 588 | :DOCKER - [0:0] 589 | -A PREROUTING -m addrtype --dst-type LOCAL -j DOCKER 590 | -A OUTPUT ! -d 127.0.0.0/8 -m addrtype --dst-type LOCAL -j DOCKER 591 | -A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j MASQUERADE 592 | COMMIT 593 | # Completed on Thu Aug 6 13:41:52 2015 594 | # Generated by iptables-save v1.4.21 on Thu Aug 6 13:41:52 2015 595 | *filter 596 | :INPUT ACCEPT [0:0] 597 | :FORWARD ACCEPT [0:0] 598 | :OUTPUT ACCEPT [18:2944] 599 | :DOCKER - [0:0] 600 | -A INPUT -m state --state RELATED,ESTABLISHED -j ACCEPT 601 | -A INPUT -p icmp -j ACCEPT 602 | -A INPUT -i lo -j ACCEPT 603 | -A INPUT -p tcp -m state --state NEW -m tcp --dport 22 -j ACCEPT 604 | -A INPUT -j REJECT --reject-with icmp-host-prohibited 605 | -A FORWARD -o docker0 -j DOCKER 606 | -A FORWARD -o docker0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT 607 | -A FORWARD -i docker0 ! -o docker0 -j ACCEPT 608 | -A FORWARD -i docker0 -o docker0 -j ACCEPT 609 | -A FORWARD -j REJECT --reject-with icmp-host-prohibited 610 | COMMIT 611 | # Completed on Thu Aug 6 13:41:52 2015 612 | ``` 613 | 614 | 可以看到在还没启动任何容器的情况下,nat和filter这两个表中都添加了规则,我们来看下这些规则。首先我们再来复习下iptables的一些基本知识,当网卡收到一个数据包后,内核会决定这个数据包是发送给本地的程序,还是转发给其他的主机。如果这个数据包是发给本机的,则会的走:mangle-PREROUTING -> nat-PREROUTING -> mangle-INPUT -> filter-INPUT这个顺序,然后走到接收的应用。如果一个数据包是从本机发送出去的,那么会走:mangle-OUTPUT -> nat-OUTPUT -> filter-OUTPUT -> mangle-POSTROUTING -> nat-POSTROUTING这个顺序。如果一个数据包收到后是要转发出去的,则会走mangle-PREROUTING -> nat-PREROUTING -> mangle-FORWARD -> filter-FORWARD -> mangle-POSTROUTING -> nat-POSTROUTING这个顺序。有了这个基础后我们看下上面docker的iptables规则。也来看三种情况: 615 | 616 | 1. 数据包发送到本机。根据我们上面的顺序,会的走mangle-PREROUTING -> nat-PREROUTING -> mangle-INPUT -> filter-INPUT。从nat表中可以看到-A PREROUTING -m addrtype --dst-type LOCAL -j DOCKER,也就是说这里通过addrtype这个module来匹配数据包,如果这个数据包是发给本地的那么走DOCKER的链。目前我们的DOCKER链是空的。 617 | 2. 数据包发送出去。根据我们上面的顺序,会的走mangle-OUTPUT -> nat-OUTPUT -> filter-OUTPUT -> mangle-POSTROUTING -> nat-POSTROUTING。这里我们看到两条规则-A OUTPUT ! -d 127.0.0.0/8 -m addrtype --dst-type LOCAL -j DOCKER以及-A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j MASQUERADE。第一条规则表明如果这个数据包的目的地址是非127的LOCAL地址则走DOCKER,第二天是个SNAT,表示非docker0这个口发送的源地址网段为172.17.0.0/16的包在出去的时候会走NAT。这个规则应该保证了我们docker的容器可以访问外网。 618 | 3. 数据包的转发。根据我们上面的顺序,会的走mangle-PREROUTING -> nat-PREROUTING -> mangle-FORWARD -> filter-FORWARD -> mangle-POSTROUTING -> nat-POSTROUTING这个顺序。这里有四条规则是和DOCKER相关的,最中要的作用是说如果一个数据包的目的设备是docker0,则走DOCKER链。 619 | 620 | 现在我们启动一个容器,不指定任何的网络参数: 621 | 622 | ``` 623 | [root@dev ~]# docker run -dit --name test-os docker.io/centos /bin/bash 624 | [/code] 625 | 可以看到docker0这个bridge中被plug了一个veth: 626 | [code lang="bash"] 627 | [root@dev ~]# brctl show 628 | bridge name bridge id STP enabled interfaces 629 | docker0 8000.56847afe9799 no vethe7cd2e3 630 | virbr0 8000.52540027e8bc yes virbr0-nic 631 | [root@dev ~]# ip l show dev vethe7cd2e3 632 | 16: vethe7cd2e3: mtu 1500 qdisc noqueue master docker0 state UP mode DEFAULT 633 | link/ether f6:b9:36:fb:5b:ac brd ff:ff:ff:ff:ff:ff 634 | [root@dev ~]# ip a show dev vethe7cd2e3 635 | 16: vethe7cd2e3: mtu 1500 qdisc noqueue master docker0 state UP 636 | link/ether f6:b9:36:fb:5b:ac brd ff:ff:ff:ff:ff:ff 637 | inet6 fe80::f4b9:36ff:fefb:5bac/64 scope link 638 | valid_lft forever preferred_lft forever 639 | ``` 640 | 641 | 而veth的另一头则在我们的test-os的namespace中。需要注意的是docker对namespace的操作是直接通过套接字发送到内核的,没有像Neutron一样使用ip命令。根据之前的文章我们知道ip命令能查看到的namespace必须是ip自己建立的,因此这里不能直接通过ip命令查看namespace。 642 | 643 | 我们看下iptables此时的规则变化: 644 | 645 | ``` 646 | [root@dev ~]# iptables-save 647 | # Generated by iptables-save v1.4.21 on Thu Aug 6 14:12:58 2015 648 | *nat 649 | :PREROUTING ACCEPT [5481:502017] 650 | :INPUT ACCEPT [4:256] 651 | :OUTPUT ACCEPT [57:4519] 652 | :POSTROUTING ACCEPT [57:4519] 653 | :DOCKER - [0:0] 654 | -A PREROUTING -m addrtype --dst-type LOCAL -j DOCKER 655 | -A OUTPUT ! -d 127.0.0.0/8 -m addrtype --dst-type LOCAL -j DOCKER 656 | -A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j MASQUERADE 657 | COMMIT 658 | # Completed on Thu Aug 6 14:12:58 2015 659 | # Generated by iptables-save v1.4.21 on Thu Aug 6 14:12:58 2015 660 | *filter 661 | :INPUT ACCEPT [0:0] 662 | :FORWARD ACCEPT [0:0] 663 | :OUTPUT ACCEPT [1375:163760] 664 | :DOCKER - [0:0] 665 | -A INPUT -m state --state RELATED,ESTABLISHED -j ACCEPT 666 | -A INPUT -p icmp -j ACCEPT 667 | -A INPUT -i lo -j ACCEPT 668 | -A INPUT -p tcp -m state --state NEW -m tcp --dport 22 -j ACCEPT 669 | -A INPUT -j REJECT --reject-with icmp-host-prohibited 670 | -A FORWARD -o docker0 -j DOCKER 671 | -A FORWARD -o docker0 -m conntrac`k --ctstate RELATED,ESTABLISHED -j ACCEPT 672 | -A FORWARD -i docker0 ! -o docker0 -j ACCEPT 673 | -A FORWARD -i docker0 -o docker0 -j ACCEPT 674 | -A FORWARD -j REJECT --reject-with icmp-host-prohibited 675 | COMMIT 676 | # Completed on Thu Aug 6 14:12:58 2015 677 | ``` 678 | 679 | 可以看到iptables的规则没有发生变化。此时如果容器要发送请求给公网,那么这个请求会通过veth走到物理机namespace的vethe7cd2e3。由于这个vethe7cd2e3是plug到docker0上的,所以会的走网桥的逻辑。由于我们的内核设置了允许ip forward,所以这个包会从docker0出来传递到内核。对于内核来说这是一次forward的请求,所以会的走转发链,也就是走mangle-PREROUTING -> nat-PREROUTING -> mangle-FORWARD -> filter-FORWARD -> mangle-POSTROUTING -> nat-POSTROUTING。如果在容器中ping 1.2.4.8,iptables的统计信息中可以看到: 680 | 681 | ``` 682 | [root@dev ~]# iptables -t nat -L -xvn 683 | Chain PREROUTING (policy ACCEPT 6518 packets, 593756 bytes) 684 | pkts bytes target prot opt in out source destination 685 | 5 316 DOCKER all -- * * 0.0.0.0/0 0.0.0.0/0 ADDRTYPE match dst-type LOCAL 686 | 687 | Chain INPUT (policy ACCEPT 4 packets, 256 bytes) 688 | pkts bytes target prot opt in out source destination 689 | 690 | Chain OUTPUT (policy ACCEPT 58 packets, 4574 bytes) 691 | pkts bytes target prot opt in out source destination 692 | 0 0 DOCKER all -- * * 0.0.0.0/0 !127.0.0.0/8 ADDRTYPE match dst-type LOCAL 693 | 694 | Chain POSTROUTING (policy ACCEPT 58 packets, 4574 bytes) 695 | pkts bytes target prot opt in out source destination 696 | 2 168 MASQUERADE all -- * !docker0 172.17.0.0/16 0.0.0.0/0 697 | 698 | Chain DOCKER (2 references) 699 | pkts bytes target prot opt in out source destination 700 | ``` 701 | 702 | 这里POSTROUTING的pkts可以看到增加。之所以这里的pkts数目和实际上的ping包个数不一致是由于conntrackd的session信息有一定的默认cache时间造成的。 703 | 704 | 再来看下物理机想要访问容器的数据链路。当我们访问容器的172.17.0.5这个地址的时候,根据路由这个数据包是走到docker0的: 705 | 706 | ``` 707 | [root@dev ~]# ip r 708 | default via 172.16.1.1 dev enp0s3 proto static metric 100 709 | default via 10.0.2.1 dev enp0s8 proto static metric 101 710 | 10.0.2.0/24 dev enp0s8 proto kernel scope link src 10.0.2.6 711 | 10.0.2.0/24 dev enp0s8 proto kernel scope link src 10.0.2.6 metric 100 712 | 172.16.1.0/24 dev enp0s3 proto kernel scope link src 172.16.1.75 metric 100 713 | 172.17.0.0/16 dev docker0 proto kernel scope link src 172.17.42.1 714 | 192.168.56.0/24 dev enp0s9 proto kernel scope link src 192.168.56.200 metric 100 715 | 192.168.100.0/24 dev enp0s10 proto kernel scope link src 192.168.100.101 metric 100 716 | 192.168.122.0/24 dev virbr0 proto kernel scope link src 192.168.122.1 717 | [/code] 718 | 当数据包走到docker0后走正常的bridge的逻辑,就能到ethe7cd2e3,然后到达对端的namespace中的veth,被我们的容器接收。对于容器与容器之间的互相访问也和这里类似。 719 | 720 | 现在再来看一个使用了网络参数的例子。比如我们希望映射物理机的8888到容器的80端口,我们来看下在这种情况下的规则变化: 721 | [code lang="bash"] 722 | [root@dev ~]# docker run -dit -p 8888:80 --name test-os2 docker.io/centos /bin/bash 723 | ``` 724 | 725 | 此时看下iptables: 726 | 727 | ``` 728 | [root@dev ~]# iptables-save 729 | # Generated by iptables-save v1.4.21 on Thu Aug 6 14:29:18 2015 730 | *mangle 731 | :PREROUTING ACCEPT [2644:222950] 732 | :INPUT ACCEPT [2588:218246] 733 | :FORWARD ACCEPT [56:4704] 734 | :OUTPUT ACCEPT [393:75016] 735 | :POSTROUTING ACCEPT [449:79720] 736 | COMMIT 737 | # Completed on Thu Aug 6 14:29:18 2015 738 | # Generated by iptables-save v1.4.21 on Thu Aug 6 14:29:18 2015 739 | *nat 740 | :PREROUTING ACCEPT [38:2854] 741 | :INPUT ACCEPT [0:0] 742 | :OUTPUT ACCEPT [1:55] 743 | :POSTROUTING ACCEPT [1:55] 744 | :DOCKER - [0:0] 745 | -A PREROUTING -m addrtype --dst-type LOCAL -j DOCKER 746 | -A OUTPUT ! -d 127.0.0.0/8 -m addrtype --dst-type LOCAL -j DOCKER 747 | -A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j MASQUERADE 748 | -A POSTROUTING -s 172.17.0.6/32 -d 172.17.0.6/32 -p tcp -m tcp --dport 80 -j MASQUERADE 749 | -A DOCKER ! -i docker0 -p tcp -m tcp --dport 8888 -j DNAT --to-destination 172.17.0.6:80 750 | COMMIT 751 | # Completed on Thu Aug 6 14:29:18 2015 752 | # Generated by iptables-save v1.4.21 on Thu Aug 6 14:29:18 2015 753 | *filter 754 | :INPUT ACCEPT [0:0] 755 | :FORWARD ACCEPT [0:0] 756 | :OUTPUT ACCEPT [29:2822] 757 | :DOCKER - [0:0] 758 | -A INPUT -m state --state RELATED,ESTABLISHED -j ACCEPT 759 | -A INPUT -p icmp -j ACCEPT 760 | -A INPUT -i lo -j ACCEPT 761 | -A INPUT -p tcp -m state --state NEW -m tcp --dport 22 -j ACCEPT 762 | -A INPUT -j REJECT --reject-with icmp-host-prohibited 763 | -A FORWARD -o docker0 -j DOCKER 764 | -A FORWARD -o docker0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT 765 | -A FORWARD -i docker0 ! -o docker0 -j ACCEPT 766 | -A FORWARD -i docker0 -o docker0 -j ACCEPT 767 | -A FORWARD -j REJECT --reject-with icmp-host-prohibited 768 | -A DOCKER -d 172.17.0.6/32 ! -i docker0 -o docker0 -p tcp -m tcp --dport 80 -j ACCEPT 769 | COMMIT 770 | # Completed on Thu Aug 6 14:29:18 2015 771 | ``` 772 | 773 | 可以看到这里多了很多和80、8888相关的规则,我们来分析下。首先来看一个数据包需要从容器发送到公网的情况,此时会走转发的逻辑,也就是我们上面说的mangle-PREROUTING -> nat-PREROUTING -> mangle-FORWARD -> filter-FORWARD -> mangle-POSTROUTING -> nat-POSTROUTING。此时数据包已经经过网桥的逻辑,从docker0进入到内核,然后走nat-PREROUTING,也就是会的走DOCKER的链,但DOCKER链! -i docker0这个要求使得这个数据包没有被匹配上,于是就继续走剩余的规则。filter中的FORWARD也没有匹配的,于是最后走nat-POSTROUTING,也就是通过SNAT出去了。现在来看下公网访问物理机的8888的流程,当这个数据包进入内核后,内核走nat-PREROUTING,然后匹配上了-A DOCKER ! -i docker0 -p tcp -m tcp --dport 8888 -j DNAT --to-destination 172.17.0.6:80,接着内核将这个数据包的目的地址改为172.17.0.6:80,接着就发送给了我们的容器。 774 | 775 | 776 | 关于Docker的笔者就简单介绍这么多。有兴趣的读者可以自行去学习。接下来我们会讲libnetwork的实现以及和Neutron相关的结合,这才是我们关注的重点。 777 | 778 | -------------------------------------------------------------------------------- /book/ch07/s7.3.md: -------------------------------------------------------------------------------- 1 | ## Kubernetes 2 | 3 | 在本章的一开始笔者提到基于容器提供一个PaaS是一个比较合适的方案,实际上目前一些大型厂商的PaaS服务就是基于类似容器的技术来提供的,而不是叠加在IaaS对外提供。当然笔者这里不讨论这样做是否合适。如果要基于容器提供PaaS服务那么必然要有一个调度控制器来管理我们的容器。目前Google的Kubernetes就是一个这样的平台。这里我们来讲一下这个Kubernetes。 4 | 5 | Kubernetes笔者个人倾向于其是一个任务调度系统。比如一个WEB应用运行在Tomcat上,现在这个应用要上线了,在Kubernetes中的做法就是将这个WEB应用及Tomcat等先做成一个容器,然后在Kubernetes里写好这个应用的一些描述。接着通过Kubernetes的指令在一个集群中启动这个应用。使用者只要关心在容器中启动应用即可,Kubernetes会的根据描述信息自动的在集群中找到合适的物理机,然后在上面下载容器镜像并启动容器。一般为了高可用我们可以在描述信息中描述好副本个数,比如我们指定这个应用可以存在三个副本,则我们在启动应用的时候Kubernetes会的在集群中启动三个相同的应用,同时Kubernetes中存在一个Service的概念,对这个概念最简单的理解就是可以把它当成一个负载均衡器,外界来访问我们的WEB应用的时候看到的是Service的地址。当集群中运行应用容器的主机异常或宕机的时候,Service会的帮我们屏蔽这种异常。同时Kubernetes会根据副本个数保证集群中始终维持要求的副本个数。 6 | 7 | 在Kubernetes中,上面讲到的应用在其术语中称为Pod。大家可以认为一个Pod就是一个独立、完全的集合体,比如我们的应用是MySQL+Apache+PHP,那么在一个Pod中可以运行一个MySQL的容器以及一个Apache+PHP的容器,然后对外提供服务的时候一个Pod就够了,不需要额外的其它容器了。 8 | 9 | 对于Kubernetes的网络是比较值得研究的一个话题。Docker由于目前局限在单物理机的状态,所以其网络并不是非常复杂。但对于Kubernetes来说由于是一个集群,因此Pod与Pod之间、Pod与外界之间的网络就必须要做到足够的强大才能满足业务的支撑。在Kubernetes中一个Pod拥有一个独立的IP,在这个Pod中的所有容器都直接使用这个IP而不需要走NAT,因此Pod中的容器看到的自身IP就是外界看到的这个容器的IP。这样做的好处是对以前运行在虚拟机环境的业务能非常容易的进行迁移。比如我们上面的MySQL+Apache+PHP的例子,在传统虚拟化里一般是启动一台虚拟机然后上面分别运行MySQL以及Apache进程,这些进程监听的IP就是虚拟机的IP。此时将这一套迁移到Pod的时候网络方面就简单很多。当Pod需要和外界进行交互的时候Kubernetes目前有很多方案,按照刚刚说的例子这里大家可以把Pod想成我们的Nova虚拟机,这样大家就可以用Neutron的思想来套Kubernetes的网络方案了。比如复杂一点的,Pod中的数据流出来后接一个OVS,然后OVS打上vxlan头发送出去。 10 | 11 | ### 基于OVS的多节点Kubernetes搭建过程 12 | 13 | 这里给出一个基于OVS的多节点Kubernetes的搭建过程,官方这部分文档目前还比较缺。一共三台主机,k8s01跑相关的控制器,k8s02和k8s03用来跑容器。首先我们先建立三台虚拟机,系统使用Fedora 22。主机信息如下: 14 | 15 | ``` 16 | 192.168.0.33 k8s01 17 | 192.168.0.34 k8s02 18 | 192.168.0.35 k8s03 19 | ``` 20 | 21 | 下面是具体的部署步骤。 22 | 23 | 首先,在分别在三台主机的/etc/hosts文件中配置好相关的主机名与IP信息: 24 | ``` 25 | [root@k8s01 ~]# echo "192.168.0.33 k8s01 26 | > 192.168.0.34 k8s02 27 | > 192.168.0.35 k8s03" >> /etc/hosts 28 | ``` 29 | 30 | 接着在三台主机上安装相关的软件包: 31 | ``` 32 | [root@k8s01 ~]# yum -y install --enablerepo=updates-testing kubernetes 33 | [root@k8s01 ~]# yum -y install etcd iptables 34 | ``` 35 | 36 | 三台主机上都把防火墙停了: 37 | ``` 38 | [root@k8s01 ~]# systemctl disable iptables-services firewalld 39 | [root@k8s01 ~]# systemctl stop iptables-services firewalld 40 | Failed to stop iptables-services.service: Unit iptables-services.service not loaded. 41 | ``` 42 | 43 | k8s02和k8s03上开启转发功能: 44 | ``` 45 | [root@k8s02 ~]# echo "1" > /proc/sys/net/ipv4/ip_forward 46 | ``` 47 | 48 | 接着我们修改下k8s的配置,首先三台主机的/etc/kubernetes/config都改为如下配置: 49 | ``` 50 | [root@k8s01 ~]# cat /etc/kubernetes/config | grep -v '#' 51 | KUBE_LOGTOSTDERR="--logtostderr=true" 52 | 53 | KUBE_LOG_LEVEL="--v=0" 54 | 55 | KUBE_ALLOW_PRIV="--allow_privileged=false" 56 | 57 | KUBE_MASTER="--master=http://k8s01:8080" 58 | ``` 59 | 60 | 生成一个key: 61 | ``` 62 | [root@k8s01 ~]# openssl genrsa -out /tmp/serviceaccount.key 2048 63 | Generating RSA private key, 2048 bit long modulus 64 | ........................................................................................+++ 65 | .........+++ 66 | e is 65537 (0x10001) 67 | ``` 68 | 69 | 接着在k8s01上修改/etc/kubernetes/apiserver如下: 70 | ``` 71 | [root@k8s01 ~]# cat /etc/kubernetes/apiserver | grep -vE '(#|^$)' 72 | KUBE_API_ADDRESS="--insecure-bind-address=0.0.0.0" 73 | KUBE_ETCD_SERVERS="--etcd_servers=http://127.0.0.1:4001" 74 | KUBE_SERVICE_ADDRESSES="--service-cluster-ip-range=10.254.0.0/16" 75 | KUBE_ADMISSION_CONTROL="--admission_control=NamespaceLifecycle,NamespaceExists,LimitRanger,SecurityContextDeny,ServiceAccount,ResourceQuota" 76 | KUBE_API_ARGS="--service_account_key_file=/tmp/serviceaccount.key" 77 | ``` 78 | 79 | 在k8s01上修改/etc/kubernetes/controller-manager如下: 80 | ``` 81 | [root@k8s01 ~]# cat /etc/kubernetes/controller-manager | grep -vE '(#|^$)' 82 | KUBE_CONTROLLER_MANAGER_ARGS="--service_account_private_key_file=/tmp/serviceaccount.key" 83 | ``` 84 | 85 | 在k8s01上修改etcd的监听端口,将/etc/etcd/etcd.conf中的ETCD_LISTEN_CLIENT_URLS改为"http://0.0.0.0:4001": 86 | ``` 87 | [root@k8s01 ~]# cat /etc/etcd/etcd.conf | grep ETCD_LISTEN_CLIENT_URLS 88 | ETCD_LISTEN_CLIENT_URLS="http://0.0.0.0:4001" 89 | ``` 90 | 91 | 在k8s01上启动对应的控制服务: 92 | ``` 93 | [root@k8s01 ~]# for SERVICES in etcd kube-apiserver kube-controller-manager kube-scheduler; do 94 | > systemctl restart $SERVICES 95 | > systemctl enable $SERVICES 96 | > systemctl status $SERVICES 97 | > done 98 | ``` 99 | 100 | 在k8s02和k8s03上修改/etc/kubernetes/kubelet文件,内容如下: 101 | ``` 102 | [root@k8s02 ~]# cat /etc/kubernetes/kubelet | grep -vE '(#|^$)' 103 | KUBELET_ADDRESS="--address=0.0.0.0" 104 | KUBELET_HOSTNAME="--hostname_override=k8s02" 105 | KUBELET_API_SERVER="--api_servers=http://k8s01:8080" 106 | KUBELET_ARGS="" 107 | ``` 108 | 109 | 在k8s02及k8s03上启动相关服务: 110 | ``` 111 | [root@k8s02 ~]# for SERVICES in kube-proxy kubelet docker; do 112 | > systemctl restart $SERVICES 113 | > systemctl enable $SERVICES 114 | > systemctl status $SERVICES 115 | > done 116 | ``` 117 | 118 | 现在我们来设置网络环境。k8s02和k8s03的两个docker0会通过veth接到各自的ovs的bridge上,然后这两个bridge通过vxlan打通。下面具体的设置步骤: 119 | 首先先在k8s02和k8s03上安装软件包: 120 | ``` 121 | [root@k8s02 ~]# yum install -y bridge-utils openvswitch 122 | ``` 123 | 124 | 需要改变下docker0的默认网段,目前在k8s02以及k8s03上docker0都是使用了172.17.42.1/16。我们改成k8s02使用172.17.20.1/24,k8s03使用172.17.30.1/24。下面是具体步骤: 125 | ``` 126 | # k8s02 127 | [root@k8s02 ~]# systemctl stop docker.service 128 | [root@k8s02 ~]# ip l set dev docker0 down 129 | [root@k8s02 ~]# brctl delbr docker0 130 | [root@k8s02 ~]# nohup docker -d --bip=172.17.20.1/24 --fixed-cidr=172.17.20.0/24 & 131 | [root@k8s03 ~]# ip r add 172.17.30.0/24 dev docker0 132 | 133 | # k8s03 134 | [root@k8s03 ~]# systemctl stop docker.service 135 | [root@k8s03 ~]# ip l set dev docker0 down 136 | [root@k8s03 ~]# brctl delbr docker0 137 | [root@k8s03 ~]# nohup docker -d --bip=172.17.30.1/24 --fixed-cidr=172.17.30.0/24 & 138 | [root@k8s02 ~]# ip r add 172.17.20.0/24 dev docker0 139 | ``` 140 | 141 | 在k8s02和k8s03上启动ovs服务: 142 | ``` 143 | [root@k8s02 ~]# systemctl start openvswitch.service 144 | ``` 145 | 146 | 在k8s02和k8s03上各自建立一个ovs的bridge br-tun: 147 | ``` 148 | [root@k8s02 ~]# ovs-vsctl add-br br-tun 149 | ``` 150 | 151 | 在k8s02和k8s03上建立veth对,连接br-tun和docker0: 152 | ``` 153 | [root@k8s02 ~]# ip link add veth-docker type veth peer name veth-ovs 154 | [root@k8s02 ~]# ovs-vsctl add-port br-tun veth-ovs 155 | [root@k8s02 ~]# brctl addif docker0 veth-docker 156 | [root@k8s02 ~]# ip l set dev veth-ovs up 157 | [root@k8s02 ~]# ip l set dev veth-docker up 158 | ``` 159 | 160 | 接着我们配置下k8s02和k8s03之间的vxlan隧道,k8s02上进行如下配置: 161 | ``` 162 | [root@k8s02 ~]# ovs-vsctl add-port br-tun port-vxlan -- set Interface port-vxlan type=vxlan options:remote_ip=192.168.0.35 163 | ``` 164 | 165 | k8s03上进行如下配置: 166 | ``` 167 | [root@k8s03 ~]# ovs-vsctl add-port br-tun port-vxlan -- set Interface port-vxlan type=vxlan options:remote_ip=192.168.0.34 168 | ``` 169 | 170 | 现在我们来测试下vxlan隧道是否起作用,我们在k8s03上建立一个veth对,然后将veth的一头放到docker0上,另一头我们配置一个ip。接着我们尝试在k8s02上ping这个地址,如果一切正常的话应该是可以ping通的: 171 | ``` 172 | # k8s03上的操作 173 | [root@k8s03 ~]# ip link add debug-docker type veth peer name debug-host 174 | [root@k8s03 ~]# brctl addif docker0 debug-docker 175 | [root@k8s03 ~]# ip l set dev debug-docker up 176 | [root@k8s03 ~]# ip l set dev debug-host up 177 | [root@k8s03 ~]# ip a change 172.17.30.99 dev debug-host 178 | 179 | # k8s02上的操作 180 | [root@k8s02 ~]# ping 172.17.30.99 -c 4 181 | PING 172.17.30.99 (172.17.30.99) 56(84) bytes of data. 182 | 64 bytes from 172.17.30.99: icmp_seq=1 ttl=64 time=1.94 ms 183 | 64 bytes from 172.17.30.99: icmp_seq=2 ttl=64 time=0.860 ms 184 | 64 bytes from 172.17.30.99: icmp_seq=3 ttl=64 time=0.846 ms 185 | 64 bytes from 172.17.30.99: icmp_seq=4 ttl=64 time=0.577 ms 186 | 187 | --- 172.17.30.99 ping statistics --- 188 | 4 packets transmitted, 4 received, 0% packet loss, time 3001ms 189 | rtt min/avg/max/mdev = 0.577/1.056/1.944/0.526 ms 190 | ``` 191 | 192 | 可以看到现在到172.17.30.99的数据包会的通过k8s02上的docker0(这是由于我们上面配置了路由),然后通过veth到br-tun,接着br-tun通过vxlan到k8s03上的br-tun,后者再通过veth到k8s03上的docker0,然后docker0桥内转发数据包给了debug-docker,后者通过veth发给了其peer口,也就是debug-host。如果大家这一步可以ping通的话我们的这个测试用的网络环境基本就可以了。 193 | 194 | 下面我们在k8s02及k8s03上下载Kubernetes需要的pause镜像。因为这个镜像是托管在google的gcr上的,后者被墙掉了,因此需要用下面的方法绕绕一下: 195 | ``` 196 | [root@k8s02 ~]# docker pull docker.io/kubernetes/pause 197 | Trying to pull repository docker.io/kubernetes/pause ... 198 | 6c4579af347b: Download complete 199 | 511136ea3c5a: Download complete 200 | e244e638e26e: Download complete 201 | Status: Downloaded newer image for docker.io/kubernetes/pause:latest 202 | [root@k8s02 ~]# docker tag docker.io/kubernetes/pause gcr.io/google_containers/pause:0.8.0 203 | ``` 204 | 205 | 然后再再k8s02及k8s03上面下个nginx的镜像用于下面的测试: 206 | ``` 207 | [root@k8s02 ~]# docker pull nginx 208 | ``` 209 | 210 | 下面来试下用Kubernetes建立个service测试下我们的网络环境是否符合要求。相关的配置文件如下: 211 | ``` 212 | [root@k8s01 ~]# cat nginx-rc.yaml 213 | apiVersion: v1 214 | kind: ReplicationController 215 | metadata: 216 | name: nginx-controller 217 | spec: 218 | replicas: 1 219 | selector: 220 | app: nginx 221 | template: 222 | metadata: 223 | labels: 224 | app: nginx 225 | spec: 226 | containers: 227 | - name: nginx 228 | image: nginx 229 | ports: 230 | - containerPort: 80 231 | [root@k8s01 ~]# cat nginx-service.yaml 232 | apiVersion: v1 233 | kind: Service 234 | metadata: 235 | name: nginx-service 236 | spec: 237 | ports: 238 | - port: 8000 239 | targetPort: 80 240 | protocol: TCP 241 | selector: 242 | app: nginx 243 | ``` 244 | 这里我们设置replicas为1,这个设置可以让我们观察我们的网络环境是否正常。 245 | 246 | 现在测试建立下这个rc和service,首先需要建立node。如果发现异常的话可以尝试重新启动下kubelet服务: 247 | ``` 248 | [root@k8s01 ~]# cat node01.json 249 | { 250 | "apiVersion": "v1", 251 | "kind": "Node", 252 | "metadata": { 253 | "name": "k8s02", 254 | "labels":{ "name": "kub-node-label"} 255 | }, 256 | "spec": { 257 | "externalID": "k8s02" 258 | } 259 | } 260 | [root@k8s01 ~]# cat node02.json 261 | { 262 | "apiVersion": "v1", 263 | "kind": "Node", 264 | "metadata": { 265 | "name": "k8s03", 266 | "labels":{ "name": "kub-node-label"} 267 | }, 268 | "spec": { 269 | "externalID": "k8s03" 270 | } 271 | } 272 | [root@k8s01 ~]# kubectl create -f ./node01.json 273 | node "k8s02" created 274 | [root@k8s01 ~]# kubectl create -f ./node02.json 275 | node "k8s03" created 276 | [root@k8s01 ~]# kubectl get nodes 277 | NAME LABELS STATUS 278 | k8s01 kubernetes.io/hostname=k8s01 Ready 279 | k8s02 name=kub-node-label Ready 280 | k8s03 name=kub-node-label Ready 281 | ``` 282 | 283 | 现在建立rc和service: 284 | ``` 285 | [root@k8s01 ~]# kubectl create -f ./nginx-rc.yaml 286 | replicationcontroller "nginx-controller" created 287 | [root@k8s01 ~]# kubectl get rc 288 | CONTROLLER CONTAINER(S) IMAGE(S) SELECTOR REPLICAS 289 | nginx-controller nginx nginx app=nginx 1 290 | [root@k8s01 ~]# kubectl get po 291 | NAME READY STATUS RESTARTS AGE 292 | nginx-controller-zfd64 1/1 Running 0 25s 293 | [root@k8s01 ~]# kubectl create -f ./nginx-service.yaml 294 | service "nginx-service" created 295 | [root@k8s01 ~]# kubectl get service 296 | NAME LABELS SELECTOR IP(S) PORT(S) 297 | kubernetes component=apiserver,provider=kubernetes 10.254.0.1 443/TCP 298 | nginx-service app=nginx 10.254.85.157 8000/TCP 299 | ``` 300 | 301 | 现在在k8s02及k8s03上curl下10.254.85.157:8000,发现都可以获取到nginx的欢迎页面: 302 | ``` 303 | # k8s02上 304 | [root@k8s02 ~]# curl 10.254.85.157:8000 305 | 306 | 307 | 308 | Welcome to nginx! 309 | 316 | 317 | 318 |

Welcome to nginx!

319 |

If you see this page, the nginx web server is successfully installed and 320 | working. Further configuration is required.

321 | 322 |

For online documentation and support please refer to 323 | nginx.org.
324 | Commercial support is available at 325 | nginx.com.

326 | 327 |

Thank you for using nginx.

328 | 329 | 330 | 331 | # k8s03上 332 | [root@k8s03 ~]# curl 10.254.85.157:8000 333 | 334 | 335 | 336 | Welcome to nginx! 337 | 344 | 345 | 346 |

Welcome to nginx!

347 |

If you see this page, the nginx web server is successfully installed and 348 | working. Further configuration is required.

349 | 350 |

For online documentation and support please refer to 351 | nginx.org.
352 | Commercial support is available at 353 | nginx.com.

354 | 355 |

Thank you for using nginx.

356 | 357 | 358 | ``` 359 | 360 | 同时观察容器运行情况,可以看到nginx目前是建立在了k8s03上: 361 | ``` 362 | # k8s02上 363 | [root@k8s02 ~]# docker ps 364 | CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 365 | [root@k8s02 ~]# 366 | 367 | # k8s03上 368 | [root@k8s03 ~]# docker ps 369 | CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 370 | 9ea0c512a8cb nginx "nginx -g 'daemon of 4 minutes ago Up 4 minutes k8s_nginx.6420169f_nginx-controller-zfd64_default_eab9db09-40ca-11e5-b237-fa163ee101cd_f4ab7b51 371 | e4fb6592bfee gcr.io/google_containers/pause:0.8.0 "/pause" 4 minutes ago Up 4 minutes k8s_POD.ef28e851_nginx-controller-zfd64_default_eab9db09-40ca-11e5-b237-fa163ee101cd_19ed5eed 372 | [root@k8s03 ~]# 373 | ``` 374 | 375 | 可以看到k8s03上负责pod网络的pause也一起起来了,另外由于上面在k8s02上可以访问到我们的nginx,说明我们的ovs网络也起作用了。如果观察iptables规则的话可以发现service的vip的地址都被代理到了kube-proxy。此时我们的一个基于openvswitch的Kubernetes多节点环境就算是搭建好了。 376 | 377 | 378 | 379 | 380 | 关于Kubernetes就介绍这么多。笔者个人认为对于希望从事大规模的集群运维的读者来说,熟悉Kubernetes这类调度控制平台的设计思想要比熟悉Docker这类容器技术重要的多。 -------------------------------------------------------------------------------- /book/ch07/s7.4.md: -------------------------------------------------------------------------------- 1 | ## libnetwork 2 | 3 | 在本章前面介绍Docker的时候,我们看到Docker中的网络相关的操作是由libnetwork来实现的。这里我们来介绍下这个libnetwork的一些概念以及使用方法。目前libnetwork的开发速度非常快,读者如果有兴趣深入的话可以以本节的内容为基础然后自行的去研究最新的实现。 4 | 5 | 目前关于libnetwork的文档基本上只有其GitHub上的docs目录下的一些文件以及README.md文件。下面的内容基本上很多都可以这些文档中找到。 6 | 7 | 8 | ### libnetwork开发环境的搭建 9 | 10 | 首先先来看下如何使用libnetwork这个库。直接从Docker中学习用法当然是可以的,不过那个会需要对Docker有一定的熟悉,所以这里笔者先列了一个测试、开发环境的搭建方法,便于大家以后研究。 11 | 12 | 首先下载libnetwork的代码,以后要开发的话直接在这个代码上进行开发即可: 13 | ``` 14 | [root@dev tmp]# git clone https://github.com/docker/libnetwork.git 15 | 正克隆到 'libnetwork'... 16 | remote: Counting objects: 4281, done. 17 | remote: Compressing objects: 100% (9/9), done. 18 | remote: Total 4281 (delta 1), reused 0 (delta 0), pack-reused 4272 19 | 接收对象中: 100% (4281/4281), 4.73 MiB | 95.00 KiB/s, done. 20 | 处理 delta 中: 100% (2001/2001), done. 21 | ``` 22 | 23 | 接着我们建立一个项目来存放我们的调试用代码,首先建立如下目录结构: 24 | ``` 25 | [root@dev bingo]# pwd 26 | /tmp/bingo 27 | [root@dev bingo]# ls 28 | bin lib Makefile src 29 | ``` 30 | 31 | Makefile内容如下: 32 | ``` 33 | [root@dev bingo]# cat Makefile 34 | .PHONE: build clean 35 | 36 | GOPATH := ${PWD}/lib:${GOPATH} 37 | export GOPATH 38 | 39 | build: 40 | go build -v -o ./bin/bingo ./src/bingo 41 | 42 | clean: 43 | rm -f ./bin/bingo 44 | ``` 45 | 46 | src目录下存放我们的调试用代码,目录结构及调试文件内容如下: 47 | ``` 48 | [root@dev src]# tree 49 | . 50 | └── bingo 51 | └── bingo.go 52 | 53 | 1 directory, 1 file 54 | [root@dev src]# cat bingo/bingo.go 55 | package main 56 | 57 | import ( 58 | "fmt" 59 | 60 | "libnetwork" 61 | ) 62 | 63 | func main() { 64 | controller, err := libnetwork.New() 65 | if err != nil { 66 | fmt.Println(err) 67 | } 68 | fmt.Println(controller) 69 | } 70 | ``` 71 | 72 | lib目录存放我们的依赖包,首先将我们上面git下来的libnetwork放到lib下的src目录下,然后从Docker的代码处拷贝其它的依赖到对应目录,并将Docker中的libnetwork替换为到我们git下来的libnetwork目录的软链: 73 | ``` 74 | [root@dev src]# pwd 75 | /tmp/bingo/lib/src 76 | [root@dev src]# ls 77 | github.com libnetwork 78 | [root@dev github.com]# ls 79 | armon coreos fluent godbus gorilla hashicorp microsoft natefinch samuel syndtr tinylib 80 | BurntSushi docker go-check golang Graylog2 kr mistifyio philhofer Sirupsen tchap vishvananda 81 | [root@dev github.com]# cd docker/ 82 | [root@dev docker]# ls -l 83 | 总用量 8 84 | drwxr-xr-x 4 root root 34 8月 24 23:13 distribution 85 | drwxr-xr-x 3 root root 16 8月 24 23:14 docker 86 | drwxr-xr-x 16 root root 4096 8月 24 23:13 libcontainer 87 | drwxr-xr-x 3 root root 81 8月 24 23:13 libkv 88 | lrwxrwxrwx 1 root root 29 8月 24 23:13 libnetwork -> /tmp/bingo/lib/src/libnetwork 89 | drwxr-xr-x 3 root root 4096 8月 24 23:13 libtrust 90 | ``` 91 | 92 | 这些都设置好后执行make编译我们的执行文件bingo: 93 | ``` 94 | [root@dev bingo]# pwd 95 | /tmp/bingo 96 | [root@dev bingo]# make 97 | go build -v -o ./bin/bingo ./src/bingo 98 | github.com/Sirupsen/logrus 99 | github.com/docker/docker/pkg/ioutils 100 | github.com/docker/docker/pkg/listenbuffer 101 | github.com/docker/libcontainer/user 102 | github.com/docker/docker/pkg/stringid 103 | github.com/docker/libkv/store 104 | github.com/BurntSushi/toml 105 | github.com/docker/libnetwork/netlabel 106 | github.com/hashicorp/consul/api 107 | github.com/coreos/go-etcd/etcd 108 | github.com/docker/docker/pkg/sockets 109 | github.com/docker/docker/pkg/tlsconfig 110 | github.com/docker/docker/pkg/plugins 111 | github.com/docker/libkv/store/etcd 112 | github.com/docker/libnetwork/config 113 | github.com/samuel/go-zookeeper/zk 114 | github.com/docker/libkv/store/consul 115 | github.com/docker/libnetwork/types 116 | github.com/docker/docker/pkg/parsers/kernel 117 | github.com/vishvananda/netlink/nl 118 | github.com/docker/libnetwork/driverapi 119 | github.com/godbus/dbus 120 | github.com/docker/libnetwork/options 121 | github.com/docker/docker/pkg/proxy 122 | github.com/vishvananda/netlink 123 | github.com/docker/docker/pkg/reexec 124 | github.com/docker/libnetwork/portallocator 125 | github.com/docker/libnetwork/drivers/host 126 | github.com/docker/libnetwork/drivers/null 127 | github.com/docker/libkv/store/zookeeper 128 | github.com/vishvananda/netns 129 | github.com/docker/libkv 130 | github.com/armon/go-metrics 131 | github.com/docker/libnetwork/datastore 132 | github.com/hashicorp/go-msgpack/codec 133 | github.com/docker/libnetwork/netutils 134 | github.com/docker/libnetwork/ipallocator 135 | github.com/docker/libnetwork/bitseq 136 | github.com/docker/libnetwork/sandbox 137 | github.com/docker/libnetwork/idm 138 | github.com/docker/libnetwork/drivers/remote/api 139 | github.com/docker/libnetwork/drivers/remote 140 | github.com/docker/libnetwork/iptables 141 | github.com/docker/libnetwork/etchosts 142 | github.com/docker/libnetwork/hostdiscovery 143 | github.com/docker/libnetwork/resolvconf/dns 144 | github.com/docker/libnetwork/resolvconf 145 | github.com/docker/libnetwork/portmapper 146 | github.com/docker/libnetwork/drivers/bridge 147 | github.com/hashicorp/memberlist 148 | github.com/hashicorp/serf/serf 149 | github.com/docker/libnetwork/drivers/overlay 150 | libnetwork 151 | _/tmp/bingo/src/bingo 152 | ``` 153 | 154 | 此时我们的调试用代码就能用上git下载下来的libnetwork包了: 155 | ``` 156 | [root@dev bingo]# bin/bingo 157 | WARN[0000] Running modprobe bridge nf_nat br_netfilter failed with message: modprobe: WARNING: Module br_netfilter not found. 158 | , error: exit status 1 159 | INFO[0000] Firewalld running: false 160 | &{map[] map[null:0xc20810bc40 overlay:0xc20810bc60 bridge:0xc20810bbc0 host:0xc20810bc00] map[] {0 0}} 161 | ``` 162 | 163 | 164 | ### libnetwork设计思想 165 | 166 | libnetwork实现了一个叫做Container Network Model (CNM)的东西,也就是说起希望成为容器的标准网络模型、框架。其包含了下面几个概念: 167 | 168 | * Sandbox。对于Sandbox大家就认为是一个namespace即可。联系我们前面Kubernetes中说的Pod,Sandbox其实就是传统意义上的虚拟机的意思。 169 | * Endpoint。Neutron中和Endpoint相对的概念我想应该是VNIC,也就是虚拟机的虚拟网卡(也可以看成是VIF)。当Sandbox要和外界通信的时候就是通过Endpoint连接到外界的,最简单的情况就是连接到一个Bridge上。 170 | * Network。libnetwork中的Network大家就认为是Neutron中的network即可,更加贴切点的话可以认为是Neutron中的一个拥有一个subnet的network。 171 | 172 | 上面这三个概念就是libnetwork的CNM的核心概念,熟悉了Neutron后并不会对这几个概念在理解上有多大问题。下面我们看下libnetwork为了对外提供这几个概念而暴露的编程结构体: 173 | 174 | * NetworkController。用于获取一个控制器,可以认为通过这个控制器可以对接下来的所有网络操作进行操作。Neutron中并没有这么一个概念,因为Neutron中的网络是由agent通过轮询或者消息的方式来间接操作的,而不是由用户使用docker命令直接在本机进行操作。 175 | * Driver。这里的Driver类似于Neutron中的core_plugin或者是ml2下的各种driver,表示的是底层网络的实现方法。比如有bridge的driver,也有基于vxlan的overlay的driver等等。这个概念和Neutron中的driver概念基本上是一样的。 176 | * Network。这里的Network结构体就是对应的上面CNM中的Network,表示建立了一个网络。通过这个结构体可以对建立的网络进行操作。 177 | * Endpoint。这里的Endpoint结构体就是对应上面CNM中的Endpoint,表示建立了一个VNIC或者是VIF。通过这个结构体可以对Endpoint进行操作。 178 | * Sandbox。这里的Sandbox结构体就是对应上面CNM中的Sandbox,表示建立了一个独立的名字空间。可以类比Nova的虚拟机或者是Kubernetes的Pod,亦或是独立的Docker容器。 179 | 180 | 181 | 接着我们看下一般使用libnetwork的方法,具体的步骤一般是下面这样的: 182 | 183 | 1. 获取一个NetworkController对象用于进行下面的操作。获取对象的时候指定Driver。 184 | 2. 通过NetworkController对象的NewNetwork()建立一个网络。这里最简单的理解就是现在我们有了一个bridge了。 185 | 3. 通过网络的CreateEndpoint()在这个网络上建立Endpoint。这里最简单的理解就是每建立一个Endpoint,我们上面建立的bridge上就会多出一个VIF口等着虚拟机或者Sandbox连上来。假设这里使用的是veth,则veth的一头目前接在了bridge中,另一头还暴露在外面。 186 | 4. 调用上面建立的Endpoint的Join方法,提供容器信息,于是libnetwork的代码就会建立一个Sandbox对象(一般这里的Sandbox就是容器的namespace,所以不会重复建立),然后将第三步建立的veth的一头接入到这个Sandbox中,也就是将其放到Sandbox的namespace中。 187 | 5. 当Sandbox的生命周期结束时,调用Endpoint的Leave方法使其从这个Network中解绑。简单的说就是将veth从Sandbox的namespace中拿出来回到物理机上。 188 | 6. 如果一个Endpoint无用了,则可以调用Delete方法删除。 189 | 7. 如果一个Network无用了,则可以调用Delete方法删除。 190 | 191 | 关于libnetwork笔者就说这么多。通过这些大家应该能基本的清楚libnetwork的基本思想、用法以及开发环境的搭建。笔者在看过libnetwork之后,相比较容器笔者就更对Kubernetes的网络感兴趣些。目前的基于libnetwork的Docker基本还是一个单机环境,虽然其roadmap中也有多节点的支持,但笔者认为从一个类库出发提供一套多节点的网络环境并不是一个最好的方法。 -------------------------------------------------------------------------------- /book/ch07/s7.5.md: -------------------------------------------------------------------------------- 1 | ## Neutron与容器 2 | 3 | 前面笔者给出了Docker的基本用法以及其网络模块libnetwork的一些介绍,对于从事Neutron开发的人来说自然会想知道能不能把Neutron融入到容器中去。笔者个人的看法是完全可以的,因为前面也说了,在Kubernetes中应用是以Pod的形式提供的,Pod基本上就可以看成是一台虚拟机。实际上如果Nova端对容器的支持足够好的话,Neutron的代码在不做改动的基础上就能支持容器的网络了,因为前面我们看到libnetwork的Sandbox对外提供的Endpoint就是一个veth,现在我们的Nova虚拟机则是对外提供一个tap然后接到qbr上的,这种接到qbr上的方法对于veth或者tap来说是没有区别的,因此对于Sandbox的veth我们也采取相同的操作即可。 4 | 5 | 当然目前社区又有人提出过一些专门的用于支持容器网络的Neutron的子项目,但是目前还没有成型。大家有兴趣的话可以持续关注下。 6 | --------------------------------------------------------------------------------