首页 > 学院 > 操作系统 > 正文

如何利用fleet单元文件为CoreOS集群创建高灵活性服务

2024-06-28 16:02:33
字体:
来源:转载
供稿:网友

提供:ZStack云计算

系列教程

本教程为CoreOS上手指南系列九篇中的第六篇。

内容简介

CoreOS能够利用一系列工具以集群化与Docker容器化方式简化服务管理工作。其中etcd负责将各独立节点联系起来并提供全局数据平台,而大部分实际服务管理任务则由fleet守护进程实现。

在上一篇教程中,我们了解了如何利用fleetctl命令操纵服务及集群成员。在今天的教程中,我们将了解如何利用单元文件定义服务。

在接下来的内容中,我们将探讨如何构建fleet单元文件,外加在生产环境下提升服务健壮性的可行方法。

先决条件

要完成本篇教程,大家首先需要拥有一套可用CoreOS集群。

在之前的系列教程如何利用DigitalOcean创建一套CoreOS集群当中,我们已经完成了集群创建工作。

此集群配置包含三个节点。它们能够利用专有网络接口实现彼此通信。这三台节点亦拥有公共接口以运行公共服务。各节点名称分别为:

coreos-1coreos-2coreos-3

虽然本教程中的大部分内容在于介绍单元文件的创建,但上述设备也将在后文说明特定指令的调度作用时被用到。

这里还建议大家参阅如何使用fleetctl一文。只有具备了fleetctl的相关知识,大家才能提交并在集群当中使用这些单元文件。

满足上述要求后,我们这就开始今天的学习之旅。

单元文件的区段与类型

由于fleet服务管理机制在很大程度上依靠本地系统的systemd init系统,因此systemd单元文件自然被用于定义各项服务。

除了服务类型之外,还有其它多种单元类型定义,且通常被作为systemd单元文件中的一个子集。每种类型都会通过一段文件后缀进行类型定义,例如example.service:

service: 这是最常见的单元文件类型。其用于定义能够运行在集群中某台设备上的服务或者应用程序。socket: 定义关于嵌套或者嵌套类文件。其中包括网络嵌套、ipC嵌套以及FIFO缓冲。当流量指向该文件时,它们负责调用服务以实现启动。device: 定义与udev设备树中可用设备相关的信息。Systemd会根据需求在各设备上根据udev规则创建device单元。其常被用于进行问题排序,从而在实际启动之前确保设备切实可用。mount: 定义与设备安装点相关的信息。其名称位于所引用安装点之后,且以破折号替代斜杠。automount: 定义挂载点。其与mount单元采用同样的命名方式,且必须配合相关的mount单元。其用于描述按需与并发安装。timer: 定义一个与其它单元关联的计时器。当此文件中设定的时间点被触发时,该相关单元即开始启动。path: 定义一条路径,其可被监控以进行基于路径的激活操作。我们可以利用它在特定路径发生变更时启动其它单元。

尽管大家可以随意选择上述选项,但service单元仍是最为常用的条目。在本教程中,我们将单纯探讨service单元的配置。

单元文件为简单的文本文件,且结尾为“.”加上以上后缀之一。文件内容由多处区段组成。在fleet当中,大部分单元文件将采用以下基本格式:

[Unit]generic_unit_directive_1generic_unit_directive_2[Service]service_specific_directive_1service_specific_directive_2service_specific_directive_3[X-Fleet]fleet_specific_directive

区段标题及单元文件中的其它内容皆区分大小写。其中[Unit]用于定义单元的常规信息。以上与各单元类型相关的选项皆可添加在这里。

[Service]区段用于设定指向各服务单元的指令。大多数(但并非全部)单元类型都与unit-type-specific区段信息相关联。大家可以参阅常见systemd单元文件man页面以了解更多与不同单元类型相关的细节。

[X-Fleet]区段用于设定该单元的调度要求以供fleet使用。利用此区段,大家可以要求某特定条件为御前 以在主机上实现单元调度。

构建主服务

在这一区段中,首先对在CoreOS上运行服务中使用过的单元文件进行调整。该文件名为apache.1.service,且内容如下:

[Unit]Description=Apache web server service# RequirementsRequires=etcd.serviceRequires=docker.serviceRequires=apache-discovery.1.service# Dependency orderingAfter=etcd.serviceAfter=docker.serviceBefore=apache-discovery.1.service[Service]# Let PRocesses take awhile to start up (for first run Docker containers)TimeoutStartSec=0# Change killmode from "control-group" to "none" to let Docker remove# work correctly.KillMode=none# Get CoreOS environmental variablesEnvironmentFile=/etc/environment# Pre-start and Start## Directives with "=-" are allowed to fail without consequenceExecStartPre=-/usr/bin/docker kill apacheExecStartPre=-/usr/bin/docker rm apacheExecStartPre=/usr/bin/docker pull username/apacheExecStart=/usr/bin/docker run --name apache -p ${COREOS_PUBLIC_IPV4}:80:80 /username/apache /usr/sbin/apache2ctl -D FOREGROUND# StopExecStop=/usr/bin/docker stop apache[X-Fleet]# Don't schedule on the same machine as other Apache instancesX-Conflicts=apache.*.service

我们首先从[Unit]区段入手。在这里,我们的基本思路是描述该单元并添加关联性信息。首先设定相关要求。在本示例中,我们将使用部分硬性约束条件。如果我们希望fleet尝试启动额外服务,但又不会因故障而导致流程中断,则可使用Wants指令。

之后,我们需要明确列出要求的先后顺序。这一点非常重要,因为某些要求需要以特定服务正在运行为前提。另外,我们也可以在这里自动利用etcd声明我们将要构建的服务。

在[Service]区段中,我们关闭服务启动超时机制。由于服务首次在主机上运行时,容器需要自Docker注册表中提取信息,而这往往会造成启动超时。其默认时长为90秒,一般来说应该是足够的,但较为复杂的容器可能需要更长的启动时间。

而后将killmode设置为none。这是因为正常的关闭模式(control-group)有时候会导致容器移除命令失效(特别是Docker的–rmoption)。这有可能在下一次重启时带来问题。

我们可以在此环境文件中找到COREOS_PUBLIC_IPV4以及COREOS_PRIVATE_IPV4环境变量(如果创建过程中启用了专有网络机制)。我们可以利用其特定主机信息轻松配置Docker容器。

ExecStartPre各行用于清空此前尚未运行的部分以实现执行环境清理。我们可以在前两行中使用=-以确保出现问题时,systemd会忽略错误并继续执行后续命令。这一点非常重要,因为我们的预启动操作基本上清除了任何先前正在运行的服务。如果找不到已经在运行的服务,则操作自然会失败——但由于其仅仅属于清理流程,因此我们不希望其影响到服务的正常执行。最后的pre-start用于确保容器始终运行最新版本。

这条启动命令会引导Docker容器并将其与主机设备的公共IPv4接口相绑定。其使用此环境文件中的信息并轻松实现接口与端口交换。这一流程采用前台运行方式,这是因为该容器会在运行中进程结束后退出。而stop命令则尝试对容器进行正常关闭。

[X-Fleet]区段包含一条简单状态,用于强制fleet在尚未运行其它Apache服务的主机上调度该服务。这是一种简单的服务高可用性实现方式,即强制要求服务启动在不同设备上。

构建主服务的几项要点

在以上示例中,我们已经进行了一些比较基础的配置。不过还有其它一些需要关注的重点。

下面来看构建主服务中需要注意的几点:

对关联性与排序逻辑进行区分:利用Requires=或者Wants=指令进行关联性排布,具体取决于如果不填写此关联性,该单元是否会失败。排序区分则可使用After=以及Before=行实现,这样我们就能轻松判断要求是否出现变更。将关联性列表与排序区分开来能帮助我们调试关联性问题。利用独立进程处理服务注册:我们的服务应当利用etcd进行注册以发挥服务发现机制的使用,并借此实现动态配置功能。不过,我们应当使用单独的“sidekick”容器以保持逻辑独立性。这样从外部视角来看,其才能提供更为明确的服务运行状态报告,并供其它组件加以参考。关注服务超时可能性:考虑调整TimeoutStartSec指令以允许更长的启动时间。将其设置为“0”则会禁用启动超时机制。我们建议这样设定,因为Docker往往需要从注册表中提取镜像(在首次运行或者发现更新时),这可能显著增加服务的初始化时长。如果服务未能彻底停止,请调整KillMode:如果我们的服务或者容器似乎无法彻底停止,请考虑使用KillMode选项。将其设置为“none”有时候能够解决容器停止后未被移除的问题。特别是在对容器进行命名时,由于Docker会因为存在重名容器而出现错误。查阅KillMode说明文档以获取更多信息。在启动前清理运行环境:与上一条相关,请确保在每次启动前清除环境中的全部既有Docker容器。这些清理命令行需要使用=-标记以保证不需要清理时仍能执行下一步操作。虽然我们一般都会使用docker stop对容器进行正常停止,但也可以使用docker kill以确保其被彻底清除。引入并使用特定主机信息以实现服务可移植性:如果大家需要将服务绑定至特定网络接口,请引入/etc/environment文件以访问COREOS_PUBLIC_IPV4以及COREOS_PRIVATE_IPV4。如果我们需要查看当前运行中服务所在设备的主机名称,则可使用%H系统标记。要了解更多可用标记信息,请参阅systemd标记说明文档。在[X-Fleet]区段中,只能使用%n、%N、%i与%p。

构建sidekick声明服务

现在我们已经掌握了如何构建主服务,接下来需要了解传统的“sidekick”服务。这些sidekick服务与主服务相关联,且可通过etcd作为注册服务的外部点。

这个被引用于主单元文件中的文件名为apache-discovery.1.service,内容如下:

[Unit]Description=Apache web server etcd registration# RequirementsRequires=etcd.serviceRequires=apache.1.service# Dependency ordering and bindingAfter=etcd.serviceAfter=apache.1.serviceBindsTo=apache.1.service[Service]# Get CoreOS environmental variablesEnvironmentFile=/etc/environment# Start## Test whether service is accessible and then register useful informationExecStart=/bin/bash -c '/while true; do /curl -f ${COREOS_PUBLIC_IPV4}:80; /if [ $? -eq 0 ]; then / etcdctl set /services/apache/${COREOS_PUBLIC_IPV4} /'{"host": "%H", "ipv4_addr": ${COREOS_PUBLIC_IPV4}, "port": 80}/' --ttl 30; /else / etcdctl rm /services/apache/${COREOS_PUBLIC_IPV4}; /fi; /sleep 20; /done'# StopExecStop=/usr/bin/etcdctl rm /services/apache/${COREOS_PUBLIC_IPV4}[X-Fleet]# Schedule on the same machine as the associated Apache serviceX-ConditionMachineOf=apache.1.service

Sidekick服务的实现方式与主服务基本一致。我们首先描述该单元的作用,而后为其提供关联性信息与排序逻辑。

这里要用到的新命令为BindsTo=。此命令会使该单元逐步执行启动、停止以及重启操作。基本上,这意味着我们能够在两个单元被载入至fleet当中后,通过操纵主单元同时管理这两个单元。这是一种单向机制,所以对sidekick单元进行控制不会影响到主单元。

在[Service]区段中,我们再次查找/etc/environment文件中的变量。此时ExecStart=指令基本上属于一条简短的bash脚本。它会尝试利用已经声明的接口与端口接入主服务。

如果连接成功,那么etcdctl命令会在etcd内的/services/apache中利用主机设备的公共IP地址设定一个键。该键的值为JSON对象,其中包含与该服务相关的信息。此键的过期时长为30秒,因此如果该单元意外停止,则对应服务信息亦不会驻留在etcd中。如果连接失败,那么该键会被立即移除——这是因为该服务无法被验证为可用。

此循环中包含一条20秒的sleep命令。这意味着每20秒(早于etcd的30秒键超时设定),此单元就会重新检查其主单元是否可用并重置该键。这基本上相当于对键上的TTL进行刷新,使其在接下来30秒中继续生效。

在这种情况下,stop命令只用于手动移除该键。意味着该服务注册将在主单元的stop命令由于BindsTo=指令而被镜像至此单元时被移除。

在[X-Fleet]区段中,我们需要确保此单元与主单元启动在同一台服务器上。尽管这套方案不允许该单元向远程设备报告服务可用性,但在本示例中,最重要的是确保BindsTo=指令正常起效。

构建sidekick服务的几项要点

在构建sidekick单元时,我们应当认真考虑以下几项要求:

检查主单元的实际可用性:检查主单元运行状态非常重要。不要仅靠sidekick能够正常初始化就认为主单元可用。虽然实际可用性取决于主单元的设计与功能,但检查机制越强大,注册状态的可靠性就越高。这样的检查工作应该拥有广泛的涵盖面,从检查/health端点到尝试利用客户端接入数据库。定期对注册逻辑进行重新检查:检查服务可用性当然很重要,但定期进行复查同样重要。我们可以借此发现意料之外的服务故障,特别是在容器中的某些结果仍在继续运行时。在不同周期间进行暂停,并借此对主单元上的其它负载进行快速审查。使用TTL标记通过etcd对故障进行自动注销:sidekick单元中的意外故障可能导致etcd内信息与实际信息间发生冲突。为了避免这种冲突,我们应当允许键超时。利用以上循环间隔机制,我们可以在各键超时前对其进行刷新,从而确保其永远不会在sidekick正常运行时过期。这种休眠间隔应被设定为略短于超时间隔,从而确保功能正常起效。利用etcd注册可用信息,而非单纯确认:在对sidekick进行首次迭代时,我们往往只关注单元启动时是否确切注册至etcd。然而其中也包含有大量可资其它服务利用的信息。虽然我们目前并不需要这些信息,但其可能会在构建其它组件时发挥作用。Etcd服务属于一项全局性键-值存储机制,因此别忘记利用它提供键信息。以JSON对象方式存储细节是传递多条信息的好办法。

掌握了以上几条要点,我们就能构建强大的注册单元,同时确保etcd拥有正常的信息可以使用。

与fleet相关的思考

虽然fleet单元文件与其它常见systemd单元文件并没有太大区别,但其中仍存在着一些值得关注的特性与陷阱。

其中最大的区别就是[X-Fleet]的存在,其可用于指引fleet制定调度决策。具体可用选项包括:

X-ConditionMachineID: 其可用于指定某台设备以进行单元加载。其提供的值为完整的设备ID。此值可由集群内单一成员通过检查/etc/machine-id文件进行检索,或者通过fleetctl的list-machines -l命令查看。其必须使用完整的ID字符串。如果大家是在特定设备上运行一套数据库及其数据目录,那么这一要求就非常必要了。当然,在非必要情况下不推荐大家使用它,因为其会严重影响到单元的灵活性。X-ConditionMachineOf: 用于将当前单元调度至加载有其它特定单元的设备之上。其可用于调度sidekick单元或者将多个单元联系在一起。X-Conflicts: 与上一条相反,其限定当前单元文件不可被调度至特定设备上。我们可以利用它轻松实现高可用性配置,即在不同设备上运行同一服务的多个版本。X-ConditionMachineMetadata:用于根据当前可用设备的元数据指定调度要求。在fleetctl list-machines输出结果中的“METADATA”列当中,大家可以看到每台主机所设定的元数据。要设置元数据,可在服务器实例初始化时将其添加至cloud-config文件中。Global: 这是一条特殊的指令,会利用一条boolean参数标记目标单元是否应被调度至集群内的全部设备上。我们应当仅使用此指令处理元数据状态。

这些额外指令允许管理员更加灵活且有效地定义服务在可用设备上的运行方式。我们需要对其进行预先评估,而后方可在fleetctl加载阶段中将其传递至特定设备的systemd实例中。

这就带来了fleet中需要注意的另一项要点。事实上,fleetctl工具并不会对单元文件内[X-Fleet]区段之外的关联性要求进行评估。这意味着fleet中的各单元可能在协作时引发某些有趣的问题。

具体来讲,当fleetctl工具采取必要步骤以将目标单元操作至所需状态时,其不会考虑到该单元的关联性需求。

因此,如果我们分别提交了主与sidekick单元,但尚未在fleet中完成加载,那么输入fleetctl start main.service将加载并尝试启动main.serivce单元。然而,由于sidekick.service单元尚未被加载,而fleetctl又不会在加载与启动过程中评估关联性,因此main.service单元会发生错误。这是因为一旦设备上的systemd实例开始处理main.service单元,将无法在关联性评估阶段找到sidekick.service。

为了避免这种情况,我们可以同时手动启动这些服务,而非领先BindsTo=指令将sidekick引入运行状态:

fleetctl start main.service sidekick.service

另一种方式则是确保sidekick单元一定会在主单元运行时进行加载。其载入过程由设备选择,而该单元文件则被提交至本地systemd实例当中。这样能够确保关联性得到满意,而BindsTo=指令也能够正常执行以启动该辅助单元:

fleetctl load main.service sidekick.servicefleetctl start main.service

因此当fleetctl命令报错时,大家可以从以上几个角度进行排查。

实例与模板

在使用fleet时,其最为强大的概念之一就是单元模板。单元模板依赖于systemd下一种名为“实例”的特性。它们属于实例化的单元,通过处理模板单元文件被创建在运行时当中。模板文件在很大程度上类似于常规单元文件,只是其中存在几处小小的修改。如果得到正确利用,其将发挥巨大作用。

模板文件可通过在文件名中使用@来标记。大多数常规服务的文件名格式为unit.service,而模板文件的名称格式则为unit@.service。

当某单元利用模板进行实例化时,其实例标记符将位于@与.service后缀之间。此标记符可由管理员任意指定:

unit@instance_id.service

此基础单元名称可通过%p标记符在单元文件之内进行访问。同样的,给定实例标记符则可通过%i进行访问。

主单元文件即模板

这意味着我们无需像之前那样一步步创建apache.1.service主单元文件,而可以直接创建一套名为apache@.service的模板:

[Unit]Description=Apache web server service on port %i# RequirementsRequires=etcd.serviceRequires=docker.serviceRequires=apache-discovery@%i.service# Dependency orderingAfter=etcd.serviceAfter=docker.serviceBefore=apache-discovery@%i.service[Service]# Let processes take awhile to start up (for first run Docker containers)TimeoutStartSec=0# Change killmode from "control-group" to "none" to let Docker remove# work correctly.KillMode=none# Get CoreOS environmental variablesEnvironmentFile=/etc/environment# Pre-start and Start## Directives with "=-" are allowed to fail without consequenceExecStartPre=-/usr/bin/docker kill apache.%iExecStartPre=-/usr/bin/docker rm apache.%iExecStartPre=/usr/bin/docker pull username/apacheExecStart=/usr/bin/docker run --name apache.%i -p ${COREOS_PUBLIC_IPV4}:%i:80 /username/apache /usr/sbin/apache2ctl -D FOREGROUND# StopExecStop=/usr/bin/docker stop apache.%i[X-Fleet]# Don't schedule on the same machine as other Apache instancesX-Conflicts=apache@*.service

如大家所见,我们将apache-discovery.1.service关联性修改为apache-discovery@%i.service。这意味着如果我们拥有此单元文件的一个名为apache@8888.service的实例,那么其将需要一个名为apache-discovery@8888.service的sidekick单元。其中的%i已经被替换为实例标记符。在这种情况下,我们使用该标记符代表与当前所运行服务相关的动态信息,特别是Apache服务器的可用端口编号。

为了实现这一目标,我们可以变更docker的运行参数,从而将该容器的端口声明至主机上的某个端口。在该静态单元文件中,我们使用的参数为COREOSPUBLICIPV4:80:80,其负责将该容器的端口80映射至主机端口80的公共IPv4接口。在此模板文件中,我们可以将其替换成{COREOS_PUBLIC_IPV4}:%i:80,因为我们会利用实例标记符来告知所使用的具体端口。总而言之,使用实例标记符能够在模板文件中实现更理想的灵活性。

Docker名称本身也进行了修改,这样它也会使用基于实例ID的惟一容器名称。请注意,Docker容器无法使用@标记,因此我们必须在单元文件中选择其它名称。为此,我们修改了全部在Docker容器上运行的指令。

在[X-Fleet]区段中,我们还修改了调度信息以识别这些实例化单元,而非我们之前使用的静态单元。

Sidekick单元即模板

我们也可以利用同样的方法将sidekick单元转化为模板。

我们的新sidekick单元将被命名为apache-discovery@.service,如下所示:

[Unit]Description=Apache web server on port %i etcd registration# RequirementsRequires=etcd.serviceRequires=apache@%i.service# Dependency ordering and bindingAfter=etcd.serviceAfter=apache@%i.serviceBindsTo=apache@%i.service[Service]# Get CoreOS environmental variablesEnvironmentFile=/etc/environment# Start## Test whether service is accessible and then register useful informationExecStart=/bin/bash -c '/while true; do /curl -f ${COREOS_PUBLIC_IPV4}:%i; /if [ $? -eq 0 ]; then / etcdctl set /services/apache/${COREOS_PUBLIC_IPV4} /'{"host": "%H", "ipv4_addr": ${COREOS_PUBLIC_IPV4}, "port": %i}/' --ttl 30; /else / etcdctl rm /services/apache/${COREOS_PUBLIC_IPV4}; /fi; /sleep 20; /done'# StopExecStop=/usr/bin/etcdctl rm /services/apache/${COREOS_PUBLIC_IPV4}[X-Fleet]# Schedule on the same machine as the associated Apache serviceX-ConditionMachineOf=apache@%i.service

我们也用同样的方式对sidekick单元中的内容进行了调整,从而构建这套实例化版本并保证其与正确的实例化主单元相匹配。

在curl命令中,当我们检查服务的实际可用性时,我们会利用实例ID替换静态端口80,从而保证其接入正确的位置。这一点非常重要,因为我们已经在Docker命令中对主单元的端口声明映射做出了变更。

我们还修改了“port”部分以登录至etcd,旨在保证其使用同样的实例ID。变更之后,被设定在etcd中的JSON数据将完全动态。其会提取主机名称、IP地址以及服务运行所在的端口。

最后,我们再次变更[X-Fleet]区段。我们需要确保此流程与主单元实例运行在同一台设备上。

利用模板进行单元实例化

要利用模板文件进行单元实例化,我们拥有几种不同选项。

其中fleet与systemd都能够处理符号链接,这意味着我们可以创建包含完整实例ID且指向模板文件的链接,具体如下:

ln -s apache@.service apache@8888.serviceln -s apache-discovery@.service apache-discovery@8888.service

这将创建两条链接,分别名为apache@8888.service与apache-discovery@8888.service。二者皆拥有fleet与systemd运行单元所必需的信息。不过,它们会指向回对应模板,因此我们需要再做点调整。

在此之后,我们可以利用以下fleetctl命令实现服务的提交、加载或者启动:

fleetctl start apache@8888.service apache-discovery@8888.service

如果我们不希望利用符号链接定义自己的实例,则可使用另一种fleetctl内的模板提交方式:

fleetctl submit apache@.service apache-discovery@.service

只需要将实例标记符分配给运行时,我们就能在fleetctl中以模板为基础实现单元实例化。例如,大家可以使用以下命令:

fleetctl start apache@8888.service apache-discovery@8888.service

这就消除了对符号链接的需求。部分管理员倾向于使用链接机制,因为这能保证实例文件随时可用。其同时允许我们将目录传递至fleetctl,从而一次性启动全部组件。

例如,在我们的工作目录中,大家可以为模板文件建立一个名为templates的子目录,并为实例化链接版本建立名为instances的子目录。大家甚至可以为非模板单元建立static子目录。具体命令如下:

mkdir templates instances static

而后将静态文件移动到static子目录下,而模板文件则移动对templates子目录下:

mv apache.1.service apache-discovery.1.service staticmv apache@.service apache-discovery@.service templates

在这里,大家可以创建自己需要的实例链接了。假设我们的服务运行在端口5555、6666与7777上:

cd instancesln -s ../templates/apache@.service apache@5555.serviceln -s ../templates/apache@.service apache@6666.serviceln -s ../templates/apache@.service apache@7777.serviceln -s ../templates/apache-discovery@.service apache-discovery@5555.serviceln -s ../templates/apache-discovery@.service apache-discovery@6666.serviceln -s ../templates/apache-discovery@.service apache-discovery@7777.service

而后利用以下命令即可一次性启动全部实例:

cd ..fleetctl start instances/*

非常简单,也非常快捷。

总结

到这里,大家应该已经掌握了fleet单元文件的构建方法了。利用单元文件带来的动态特性,我们能够确保自己的服务始终得到均匀分布、拥有正确的关联性并利用etcd注册使用信息。

在下一篇文章中,我们将探讨如何配置自己的容器,从而使用通过etcd注册的信息。如此一来,我们就能够建立对实际部署环境的认识,并将请求传递至后端中的合适容器当中。

本文来源自DigitalOcean Community。英文原文:How to Create Flexible Services for a CoreOS Cluster with Fleet Unit Files By Justin Ellingwood

翻译:diradw


发表评论 共有条评论
用户名: 密码:
验证码: 匿名发表