码迷,mamicode.com
首页 > Web开发 > 详细

第三章 pod:运行于kubernetes中的容器

时间:2020-03-24 23:12:05      阅读:89      评论:0      收藏:0      [点我收藏+]

标签:方法   img   cal   遇到   ima   写入   世界   子集   子进程   

本章内容涵盖

  • 创建、 启动和停止 pod
  • 使用标签组织 pod 和其他资源
  • 使用特定标签对所有 pod 执行操作
  • 使用命名空间将多个 pod 分到不重叠的组中
  • 调度 pod 到指定类型的工作节点

   上一章 已经大致介绍了在 Kubemetes 中创建的基本组件,包括它们的基本功 能概述。 那么接下来我们将更加详细地介绍所有类型的 Kubemetes 对象(或资源), 以便你理解在何时、 如何及为何要使用每一个对象。 其中 pod 是 Kubemetes 中最为 重要的核心概念,而其他对象仅仅是在管理、 暴露 pod 或被 pod 使用 ,所以我们将 首先介绍 pod 这一核心概念。

3.1 介绍pod

  我们已经了解到, pod 是一组并置的容器,代表了 Kubernetes 中的基本构建模 块。 在实际应用 中我们并不会单独部署容器, 更多的是针对一组 pod 的容器进行部 署和操作。 然而这并不意味着一个 pod 总是要包含多个容器一一实际上只包含一个单独容器的 pod 也是非常常见的。 值得注意的是,当一个 pod 包含多个容器时,这 些容器总是运行于同一个工作节点上——一个 pod 绝不会跨越多个工作节点,如图 3.1 所示。

技术图片

 

 3.1.1 为何需要 pod

  关于为何需要 pod 这种容器?为何不直接使用容器?为何甚至需要同时运行 多个容器?难道不能简单地把所有进程都放在一个单独的容器中吗?接下来我们将 一一回答上述问题。

为何多个容器比单个容器中包含多个进程要好

  想象一个由多个进程组成的应用程序,无论是通过 ipc (进程间通信)还是本 地存储文件进行通信,都要求它们运行于同一 台机器上。 在 Kubemetes 中,我们经 常在容器中运行进程,由于每一个容器都非常像一台独立的机器,此时你可能认为 在单个容器中运行多个进程是合乎逻辑的,然而在实践中这种做法并不合理。 容器被设计为每个容器只运行一个进程(除非进程本身产生子进程)。如果在 单个容器中运行多个不相关的进程,那么保持所有进程运行、管理它们的日志等将 会是我们的责任。例如,我们需要包含一种在进程崩溃时能够自动重启 的机制 。 同 时这些进程都将记录到相同的标准输出中,而此时我们将很难确定每个进程分别记 录了什么 。 

  综上所述,我们需要让每个进程运行于自己的容器中,而这就是Docker和kubernetes期望使用的方式。

3.1.2 了解 pod

  由于不能将多个进程聚集在一个单独的容器中,我们需要另一种更高级的结构 来将容器绑定在一起,并将它们作为一个单元进行管理,这就是 pod 背后的根本原理。 在包含容器的 pod 下,我们可以同时运行一些密切相关的进程,并为它们提供(几 乎)相同的环境,此时这些进程就好像全部运行于单个容器中一样,同时又保持着 一定的隔离。这样一来,我们便能全面地利用容器所提供的特性,同时对这些进程 来说它们就像运行在一起一样,实现两全其美。

  同一 pod 中容器之间的部分隔离

  在上一章中,我们己经了解到容器之间彼此是完全隔离的,但此时我们期望的 是隔离容器组,而不是单个容器,并让每个容器组内的容器共享一些资源,而不是 全部(换句话说,没有完全隔离)。 Kubemetes 通过配置 Docker 来让一个 pod 内的 所有容器共享相同的 Linux 命名空间,而不是每个容器都有自己的一组命名空间。

  由于一个 pod 中的所有容器都在相同的 network 和 UTS 命名空间下运行(在这 里我们讨论的是 Linux 命名空间),所以它们都共享相同的主机名和网络接口。同样 地,这些容器也都在相同的 IPC 命名空间下运行,因此能够通过 IPC 进行通信。在 最新的 Kubemetes 和 Docker 版本中,它们也能够共享相 同的 PID 命名空间,但是 该特征默认是未激活的。

  注意 当同一个 pod 中的容器使用单独的 PID 命名空间时,在容器中执行 ps aux 就只会看到容器自己的进程。

  但当涉及文件系统时,情况就有所不同。 由于大多数容器的文件系统来自容器 镜像,因此默认情况下,每个容器的文件系统与其他容器完全隔离。但我们可以使 用名为 Volume 的 Kubemetes 资源来共享文件目录, 关于这一概念将在第 6 章进行 讨论。

  容器如何共享相同的 IP 和端口空间

  这里需强调的一点是,由于一个 pod 中的容器运行于相同的 Network 命名空间 中,因此它们共享相同的 IP 地址和端口空间。这意味着在同- pod 中的容器运行的 多个进程需要注意不能绑定到相同的端口号,否则会导致端口冲突,但这只涉及同 - pod 中的容器。 由于每个 pod 都有独立的端口空间,对于不同 pod 中的容器来说 则永远不会遇到端口冲突。此外, 一个 pod 中的所有容器也都具有相同的 loopback 网络接口,因此容器可以通过 localhost 与同一pod 中的其他容器进行通信。

  介绍平坦 pod 间网络 

  Kubemetes 集群中的所有 pod 都在同一个共享网络地址空间中(如图 3.2 所示), 这意味着每个 pod 都可以通过其他 pod 的 IP 地址来实现相互访问 。 换句话说,这也 表示它们之间没有 NAT (网络地址转换)网关。当两个 pod 彼此之间发送网络数据 包时,它们都会将对方的实际 IP 地址看作数据包中的源 IP。

  技术图片

 

   因此, pod 之间的通信其实是非常简单的。 不论是将两个 pod 安排在单一的还 是不同的工作节点上,同时不管实际节点间的网络拓扑结构如何,这些 pod 内的容 器都能够像在无 NAT 的平坦网络中一样相互通信,就像局域网 CLAN)上的计算机 一样。 此时,每个 pod 都有自己的 IP 地址,并且可以通过这个专门的网络实现 pod 之间互相访问。这个专门的网络通常是由额外的软件基于真实链路实现的。

  总结本节涵盖的内容: pod 是逻辑主机,其行为与非容器世界中的物理主机或 虚拟机非常相似。此外,运行在同一个 pod 中的进程与运行在同一物理机或虚拟机 上的进程相似,只是每个进程都封装在一个容器之中。

3.1.3 通过 pod 合理管理容器

  将 pod 视为独立的机器,其中每个机器只托管一个特定的应用。过去我们习惯 于将各种应用程序塞进同一台主机,但是 pod 不是这么干的。 由于 pod 比较轻量, 我们可以在几乎不导致任何额外开销的前提下拥有尽可能多的 pod。与将所有内容 填充到一个 pod 中不同,我们应该将应用程序组织到多个 pod 中,而每个 pod 只包 含紧密相关的组件或进程。说到这里,对于一个由前端应用服务器和后端数据库组成的多层应用程序,你 认为应该将其配置为单个 pod 还是两个 pod 呢?下面我们将对该问题做进一步探讨。

  将多层应用分散到多个 pod 中 

  虽然我们可以在单个 pod 中同时运行前端服务器和数据库这两个容器,但这种 方式并不值得推荐。 前面我们已经讨论过,同- pod 的所有容器总是运行在一起, 但对于 Web 服务器和数据库来说,它们真的需要在同一台计算机上运行吗?答案显 然是否定的,它们不应该被放到同一个 pod 中 。 那假如你非要把它们放在一起,有错吗?某种程度上来说,是的。 

  如果前端和后端都在同一个容器中,那么两者将始终在同-台计算机上运行。 如果你有一个双节点 Kubemetes 集群, 而只有一个单独的 pod,那么你将始终只会 用一个工作节点,而不会充分利用第二个节点上的计算资源(CPU 和内存)。 因此 更合理的做法是将 pod 拆分到两个工作节点上,允许 Kubemetes 将前端安排到一个 节点,将后端安排到另一个节点,从而提高基础架构的利用率。

  另一个不应该将应用程序都放到单一pod 中的原因就是扩缩容。 pod 也是扩缩 容的基本单位,对于 Kubemetes 来说,它不能横向扩缩单个容器,只能扩缩整个 pod。这意味着如果你的 pod 由一个前端和一个后端容器组成,那么当你扩大 pod 的 实例数量时,比如扩大为两个, 最终会得到两个前端容器和两个后端容器。

  通常来说,前端组件与后端组件具有完全不同的扩缩容需求,所以我们倾向于 分别独立地扩缩它们。 更不用说,像数据库这样的后端服务器,通常比无状态的前 端 web 服务器更难扩展。 因此,如果你需要单独扩缩容器,那么这个容器很明确地 应该被部署在单独的 pod 中。

  何时在 pod 中使用多个容器 

  将多个容器添加到单个 pod 的主要原因是应用可能由一个主进程和一个或多个 辅助进程组成,如图 3.3 所示。 

技术图片

 

   例如, pod 中的主容器可以是一个仅仅服务于某个目录中的文件的 Web 服务 器,而另 一个容器(所谓的 sidecar 容器) 则定期从夕阳H源下载 内 容并将其存储在 Web 服务器目录中。在第 6 章中,我们将看到在这种情况下需要使用 Kubemetes Volume,并将其挂载到两个容器中 。 

  sidecar 容器的其他例子包括日志轮转器和收集器、数据处理器、通信适配器等。

  决定何时在 pod 中使用多个容器 

  回顾一下容器应该如何分组到 pod 中 : 当决定是将两个容器放入一个 pod 还是 两个单独的 pod 时,我们需要问自己以下问题: 

  • 它们需要一起运行还是可以在不同的主机上运行?
  • 它们代表的是一个整体还是相互独立的组件?
  • 它们必须一起进行扩缩容还是可以分别进行? 

  基本上,我们总是应该倾向于在单独的 pod 中运行容器,除非有特定的原因要 求它们是同一pod 的一部分。 图 3.4 将有助于我们记忆这一点。

技术图片

 

 

尽管 pod 可以包含多个容器,但为了保持现在的简单性, 本章将仅讨论单容器 pod 有关的问题。 稍后我们将在第 6 章看到如何在一个 pod 中使用多个容器。

3.2 以YAML或JSON描i主文件创建pod

   pod 和其他 Kubemetes 资源通常是通过向 Kubemetes REST API 提供 JSON 或 YAML 描述文件来创建的。 此外还有其他更简单的创建资源的方法, 比如在前一章中使用的 kubectl run 命令,但这些方法通常只允许你配置一组有限的属性。 另外, 通过 YAML 文件定义所有的 Kubemetes 对象之后,还可以将它们存储在版本控制系 统中,充分利用版本控制所带来的便利性。 

   因此,为了配置每种类型资源的各种属性,我们 需要了解并理解 Kubemetes API 对象定义。 通过本书学习各种资源类型 时,我们将会了解其中的大部分内 容。 需要注意的是,我们并不会解释每二个独立属性,因此在创建对象时还应参考 http://kubernetes.io/ docs/reference 中的 Kubernetes API 参考文档。

3.2.1 检查现有 pod 的 YAML 描述文件

  假设我们己经在上一章中创建了一些 pod, 接下来就来看看这些 pod 的 YAML 文件是如何定义的。我们将使用带有 -o yaml 选项的 kubectl get 命令来获取 pod 的整个 YAML 定义,正如下面的代码清单所示。

技术图片

 

 

 技术图片

 

  上述代码清单的内容看上去较为复杂,但一旦我们理解了基础知识并知道如何区分重要部分和细枝末节时,它就变得非常简单。此外,稍后我们将看到, 当创建 一个新的 pod 时, 需要写的 YAML 相对来说则要短得多。

 介绍 pod 定义的主要部分 

 pod 定义由这么几个部分组成 : 首先是 YAML中使用的 Kubernetes API 版本和 YAML 描述的资源类型;其次是几乎在所有 Kubemetes 资源中都可以找到的三大重 要部分 : 

  • metadata 包括名称、命名空间、标签和关于该容器的其他信息。 
  • spec 包含 pod 内容的实际说明, 例如 pod 的容器、卷和其他数据。 
  • status 包含运行中的 pod 的当前信息,例如 pod 所处的条件、 每个容器的描述和状态,以及内部 IP 和其他基本信息。 

   代码清单 3.1 展示了一个正在运行的 pod 的完整描述,其中包含了它的状态。 status 部分包含只读的运行时数据,该数据展示了给定时刻的资源状态。而在创 建新的 pod 时,永远不需要提供 status 部分。 上述三部分展示了 Kubemetes API 对象的典型结构。正如你将在整本书中看到 的那样,其他对象也都具有相同的结构,这使得理解新对象相对来说更加容易 。 对上述 YAML 中的每个属性进行深究的意义并不大,因此接下来我们将关注如 何创建 pod 的最基本的 YAML。

 3.2.2 为 pod 创建一个简单的 YAML 描述文件

   我们将创建一个名为 kubia-manual.yaml 的文件(可以在任意目录下创建该文 件),或者下载本书的代码档案文件,然后在 Chapter03 文件夹中找到该文件。下面 的清单展示了该文件的全部内容。

技术图片

 

 

 

  很明显,我们能够感受到该代码清单比代码清单 3.1 中的定义要简单得多。接 下来我们就对整个描述文件进行深入探讨,该文件遵循 Kubernetes API 的 vl 版本。 我们描述的资源类型是 pod,名称为 kubia-manual : 该 pod 由基于 luksa/kubia 镜像的单个容器组成。此外我们还给该容器命名,并表示它正在监昕 8080 端口。

指定容器端口 

在pod定义部分指定端口的行为纯粹是展示性的(informational)。忽略它们对于客户端是 否可以通过端口连接到 pod 不会带来任何影响。如果容器通过绑定到地址 0.0.0.0 的端口接收连接,那么即使端口未明确列出在 pod spec 中, 其他 pod 也依旧能够连接 到该端口 。 但明确定义端口仍是有意义的,在端口定义下,每个使用集群的人都可 以快速查看每个 pod 对外暴露的端口 。 此外, 我们将在本书的后续内容中看到,明 确定义端口还允许你为每个端口指定一个名称,这样一来更加方便我们使用。

使用 kubectl explain 来发现可能的 API 对象字段 

 

 

 在准备 manifest 时,可以转到 http://kubemetes.io/docs/api 上的 Kubemetes 参考文档查看每个 API 对象支持哪些属性,也可以使用 kubectl explain 命 令。 例如,当从头创建一个 pod manifest 时,可以从请求 kubectl 来解释 pod 开始:

技术图片

 

 

 Kubectl 打印出对象的解释并列出对象可以包含的属性,接下来就可以深入 了解各个属性的更多信息。 例如,可以这样查看 spec 属性 :

 技术图片

 

 

 3.2.3 使用 kubectl create 来创建 pod

 我们使用 kubectl create 命令从 YAML 文件创建 pod:

# kubectl create -f kubia-manual.yaml 
pod/kubia-manual created

kubectl create - f 命令用于从 YAML 或 ISON 文件创建任何资源(不只 是 pod)。

得到运行中 pod 的完整定义 

pod 创建完成后,可以请求kubernetes来获取完整的YAML,可以看到它与我们之前看到的YAML文件非常相似。在下一节中我们将了解返回定义中出现的其他字段,接下来就直接使用以下命令来查看该pod的完整描述文件:

# kubectl get pod kubia-manual -o yaml

也可以让 kubectl 返回 JSON 格式而不是 YAML 格式(显然,即使你使用 YAML创建 pod,同样也可以获取 JSON 格式的描述文件〉 :

# kubectl get pod kubia-manual -o json

在 pod 列表中查看新创建的 pod

创建好 pod 之后,如何知道它是否正在运行? 此时可以列出 pod 来查看它们的 状态:

技术图片

 

 这里可 以看到 kubia-manual 这个 pod, 状态显示它正在运行。 有可能你像笔 者一样想要通过与 pod 的实际通信来确认其正在运行, 但该方法将在之后进行讨论。 现在我们先查看应用的日志来检查是否存在错误。

3.2.4 查看应用程序日志

  小型 Node.js 应用将日志记录到进程的标准输出。容器化的应用程序通常会将 日志记录到标准输出和标准错误流,而不是将其写入文件,这就允许用户可以通过 简单、标准的方式查看不同应用程序的日志。 

  容器运行时(在我们的例子中为 Docker)将这些流重定向到文件,并允许我们 运行以下命令来获取容器的日志:

# docker logs <container id> 

  使用 ssh 命令登录至lj pod 正在运行的节点,并使用 docker logs 命令查看其 日志,但 Kubernetes 提供了一种更为简单的方法。

使用 kubectl logs 命令获取 pod 日志

  为了查看 pod 的日志(更准确地说是容器的日志),只需要在本地机器上运行 以下命令(不需要 ssh 到任何地方) : 

技术图片

  在我们向 Node.js 应用程序发送任何 Web 请求之前,日志只显示一条关于服 务器启动的语句。正如我们所见,如果该 pod 只包含一个容器,那么查看这种在 Kubernetes 中运行的应用程序的日志则非常简单。

  注意每天或者每次日志文件达到 10MB 大小时, 容器 日志都会自动轮替。 kubectl logs 命令仅显示最后一次轮替后的 日志条目 。

  获取多容器 pod 的日志时指定容器名称

  如果我们的 pod 包含多个容器,在运行 kubectl logs 命令时则必须通过包 含 -c <容器名称>选项来显式指定容器名称。在 kubia-manual pod 中,我们将 容器的名称设置为 kubia,所以如果该 pod 中有其他容器,可以通过如下命令获取其日志 :

技术图片

 

   这里需要注意的是,我们只能获取仍然存在的 pod 的日 志。 当一个 pod 被删除时, 它的日志也会被删除。如果希望在 pod 删除之后仍然可以获取其日志,我们需要设 置中心化的、 集群范围的日志系统,将所有日志存储到中心存储中。在第 17 章中 我们将会解释如何设置集中的日志系统。

3.2.5 向 pod 发送请求

  通过kubectl get和kubectl logs命令显示该pod正在运行,但我们如何在实际操作中看到该状态呢?在前一章中,我们使用 kubectl expose 命令创建了一 个 service,以便在外部访问该 pod。 由于有一整章专 门介绍 service,因此本章并不 打算使用该方法。 此外,还有其他连接到 pod 以进行测试和调试的方法,其中之一 便是通过端 口转发。

将本地网络端口转发歪lj pod 中的端口 

  如果想要在不通过 service 的情况下与某个特定的 pod 进行通信 ( 出于调试或 其他原因), Kubemetes 将允许我们配置端口转发到该 pod。 可以通过 kubectl port-forward 命令完成上述操作。 

  用法是: kubectl port-forward pod名 代理端口:pod端口

  例如以下命令会将机器的本地端口 8888 转发 到我们的 kubia- manual pod 的端口 8080 : 

技术图片

 

 此时端口转发正在运行,可以通过本地端口连接到我们的 pod。

通过端口转发连接到 pod

在另一个终端中,可以使用 curl 命令向 pod 发送一个 HTTP 请求:(通过运行在 localhost:8888 上的 kubectl port-forward 代理)

技术图片

 

 图 3.5 展示了发送请求时的简化视图。实际上, kubectl 进程和 pod 之间还有 一些额外的组件,但现在暂时不关注它们。

技术图片

 

   像这样使用端口转发是一种测试特定 pod 的有效方法,而我们也将在这本书中 学习其他类似的方法。

 

3.3 使用标签组织pod

  此时我们的集群中只有两个正在运行的pod。但部署实际应用程序时,大多数用户最终将运行更多的pod。随着pod数量的增加,将它们分类到子集的需求也就变得越来越明显了。

  例如,对于微服务架构,部署的微服务数量可以轻松超过20个甚至更多。这些组件可能是副本(部署同一组件的多个副本)和多个不同的发布版本 (stable、 beta、 canary 等)同时运行。这样一来可能会导致我们在系统中拥有数百个 pod, 如 果没有可 以有效组织这些组件的机制, 将会导致产生巨大的混乱, 如图 3.6 所示。 该图展示了多个微服务的 pod, 包括一些运行多副本集,以及其他运行于同一微服 务中的不同版本。

 技术图片

 

   很明显,我们需要一种能够基于任意标准将上述 pod 组织成更小群体的方式, 这样一来处理系统的每个开发人员和系统管理员都可以轻松地看到哪个 pod 是什么。 此外,我们希望通过一次操作对属于某个组的所有 pod 进行操作,而不必单独为每 个 pod 执行操作。 

   那就是通过标签来组织 pod 和所有其他 Kubemetes 对象。

 3.3.1 介绍标签

   标签是一种简单却功能强大的 Kubemetes 特性,不仅可以组织 pod,也可以组 织所有其他的 Kubemetes 资源。详细来讲, 标签是可以附加到资源的任意键值对,用以选择具有该确切标签的资源(这是通过标签选择器完成的)。 只要标签的 key 在 资源内是唯一的, 一个资源便可以拥有多个标签。 通常在我们创建资源时就会将标 签附加到资源上,但之后我们也可以再添加其他标签,或者修改现有标签的值,而 无须重新创建资源。 

   接下来回到图 3.6 中的微服务示例。 通过给这些 pod 添加标签,可以得到一个 更组织化的系统, 以便我们理解。 此时每个 pod 都标有两个标签 : 

  • app,它指定 pod 属于哪个应用、组件或微服务。 
  • rel, 它显示在 pod 中运行的应用程序版本是 stable、 beta 还是 canary。

   定义 金丝在发布是指在吾l~署新版本时,先只让一小部分用户体验新版本以观察 新版本的表现, 然后再向所有用户进行推广,这样可以防止暴露有问题的版本给过 多的用户 。

   如图 3.7 所示,通过添加这两个标签基本上可以将 pod 组织为两个维度(基于 应用的横向维度和基于版本的纵向维度)。

   技术图片

 

   每个可以访问集群的开发或运维人员都可以通过查看 pod 标签轻松看到系统的 结构,以及每个 pod 的角色。

 3.3.2 创建 pod 时指定标签

   现在,可以通过创建一个带有两个标签的新 pod 来查看标签的实际应用。使用 以下代码清单中的内容创建一个名为 kubia-manual-with-labels.yaml 的新文件。

技术图片

 

 

metadata.labels 部分己经包含了 creation method=manual 和 env=prod 标签。 现在来创建该 pod:

技术图片

 

 kubectl get pods 命令默认不会列出任何标签,但我们可以使用 --show-labels 选项来查看 :

技术图片

 如果你只对某些标签感兴趣,可以使用 L 选项指定它们并将它们分别显示在 自己的列中,而不是列出所有标签。 接下来我们再次列出所有 pod,并将附加到 pod kubia-manual-v2 上的两个标签的列展示如下:

技术图片

3.3.3 修改现有 pod 的标签

标签也可以在现有 pod 上进行添加和修改。 由于 pod kubia -manual 也是手动 创建的,所以为其添加 creation method=manual 标签 :

技术图片

 

 现在,将 kubia- manual- v2 pod 上的 env=prod 标签更改为 env=debug, 以演示现有标签也可以被更改。

 注意 在更改现有标签时 ,需要使用 --overwrite 选项

 技术图片

 

 再次列出 pod 以查看更新后的标签:

 技术图片

 

 正如我们所看到,目前将标签附加到资源上看起来并没有什么价值,在现有资 源上更改标签也是如此。 但在下一章中我们将证实,这会是一项令人难以置信的强 大功能。 而首先我们需要看看这些标签除了在列出 pod 时用以简单显示外, 还可以 用来做什么。

  

阅读本章之后,你应该对 Kubemetes 的核心模块有了系统的了解。 在接下来的 几章中学到的概念也都与 pod 有着直接关联。

在本章中,你应该已经掌握 : ·

如何决定是否应将某些容器组合在一个 pod 中。

  • pod 可以运行多个进程,这和非容器世界中的物理主机类似。
  • 可以编写 YAML 或 JSON 描述文件用于创建 pod, 然后查看 pod 的规格及其 当前状态。
  • 使用标签来组织 pod,并且一次在多个 pod 上执行操作。
  • 可以使用节点标签将 pod 只调度到提供某些指定特性的节点上。
  • 注解允许人们、工具或库将更大的数据块附加到 pod。
  • 命名空间可用于允许不同团队使用同 一 集群,就像它们使用单独的 Kubemetes 集群一样。
  • 使用 kubectl explain 命令快速查看任何 Kubernetes 资源的信息。 

第三章 pod:运行于kubernetes中的容器

标签:方法   img   cal   遇到   ima   写入   世界   子集   子进程   

原文地址:https://www.cnblogs.com/tianleblog/p/11935056.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!