文档结构  
翻译进度:已翻译     翻译赏金:10 元 (?)    ¥ 我要打赏

当谈及到互联网上最繁忙的网站时,NGINX 与 NGINX Plus 占据了市场的主导地位。事实上, NGINX 支撑了 top 1000, top 10,000, 与 top 100,000 网站中超过 50% 以上的网站。其在单个服务器上处理超过 1百万并发连接的能力促使其为 ”超大规模“ 网站与应用所采用,例如 Airbnb, Netflix, 与 Uber.

尽管 NGINX Plus 最为人所熟知的是 web 服务器,HTTP 反向代理以及负载平衡器,它也是一个支持 TCP 与 UDP 应用的全特性应用分发控制器  (ADC) 。 它的 事件驱动架构 以及使其成功应用于 HTTP 用例的其他属性也同样适用于物联网 (IoT)。

第 1 段(可获 1.56 积分)

在本文中,我们将展示 NGINX Plus 如何用于 MQTT 流量的负载均衡。MQTT 首次发布于 1999年,用于与远程油田的通信。 在2013年其被更新用于 IoT 用例,自此成为许多 IoT 部署的协议选择。 具有上百万设备的生产物联网需要高性能以及负载均衡器中的高级功能,在这个包含两个部分的系列博文中,我们将会讨论下列高级用例。

  • 负载均衡 MQTT 流量 (本篇)
    • 具有主动健康检测的高可用性
    • 使用nginScript 基于MQTT 客户端ID的会话持久化
  • 加密与授权 MQTT 流量 (coming soon)
    • TLS 终止
    • 客户端证书授权
第 2 段(可获 1.45 积分)

测试环境

为探索 NGINX Plus 的特性,我们将使用一个简单的测试环境表示一个具有 MQTT 经纪人的 IoT 环境的关键组件。此环境中的  MQTT 经纪人是运行在 Docker 容器中的 HiveMQ 实例。

The test environment for MQTT load balancing and session persistence places NGINX Plus as a TCP load balancer between MQTT clients and three HiveMQ servers in Docker containers

用于 MQTT 负载均衡与会话持久话的测试环境

对于 MQTT 经纪人而言,NGINX Plus 扮演了反向代理与负载均衡器的角度,监听默认的 1883 MQTT 端口。这向客户端提供了简单而统一的接口,而后端的 MQTT 节点可以进行扩展  (甚至下线) 而不会以任何形式影响客户端。 我们使用 Mosquitto 命令行工具 作为客户端,表示测试环境中的 IoT 设备。

第 3 段(可获 1.56 积分)

本篇以及第2部分中的所有用例使用该测试环境,而所有配置直接应用于本图中所示的结构。要了解构建该测试环境的完整指令,请参看本文结尾的附录1。

用于高可用性带有主动健康检测的负载均衡 MQTT

负载均衡器 的主要功能是应用提供高可用性,从而可以添加,删除甚至下线后台服务器而不影响客户端。 可靠实现该功能的核心是主动探测每一个后台服务器可用性的健康检测。通过主动健康检测,NGINX Plus 可以在真正的客户请求到达之前由负载均衡组中移除失效的服务器。

第 4 段(可获 1.55 积分)

健康检测的有用性依赖于它模拟真实应用流量与分析响应的精确性。 简单的服务器活动性检测,例如 ping,并不能确保后台服务正在运行。 TCP 端口开放检测并不能确保应用本身是健康的。在这里我们测试环境配置带有健康检测的基本负载均衡,以确保每台后端服务器能够接受新的 MQTT 连接。

我们将会在两个配置文件中进行修改。

在主 nginx.conf 文件中,我们包含下列的 stream 块 与 include 指令使得 NGINX Plus 由 stream_conf.d 子目录中的一个或多个文件读入 TCP 负载均衡的配置,该目录与 nginx.conf 位于相同的目录下。我们使用此种方式而不是在 nginx.conf 中包含实际的配置。

第 5 段(可获 1.76 积分)
stream {
    include stream_conf.d/*.conf;
}

然后在 nginx.conf 的相同目录下,我们创建目录 stream_conf.d 来包含我们的 TCP 与 UDP 配置文件。注意我们并没有使用已存在的 conf.d 目录,因为默认情况下该目录是为 http 配置内容而保留的,所以在其中添加 stream 配置会导致失败。

log_format mqtt '$remote_addr [$time_local] $protocol $status $bytes_received ' 
                '$bytes_sent $upstream_addr';

upstream hive_mq {
    server 127.0.0.1:18831; #node1
    server 127.0.0.1:18832; #node2
    server 127.0.0.1:18833; #node3
    zone tcp_mem 64k;
}

match mqtt_conn {
        # Send CONNECT packet with client ID "nginx health check"
        send   \x10\x20\x00\x06\x4d\x51\x49\x73\x64\x70\x03\x02\x00\x3c\x00\x12\x6e\x67\x69\x6e\x78\x20\x68\x65\x61\x6c\x74\x68\x20\x63\x68\x65\x63\x6b;
        expect \x20\x02\x00\x00; # Entire payload of CONNACK packet
}

server {
    listen 1883;
    proxy_pass hive_mq;
    proxy_connect_timeout 1s;
    health_check match=mqtt_conn;

    access_log /var/log/nginx/mqtt_access.log mqtt;
    error_log  /var/log/nginx/mqtt_error.log; # Health check notifications
}
第 6 段(可获 0.66 积分)

stream_mqtt_healthcheck.conf 中,我们首先为 MQTT 流量定义  访问日志格式  (1–2行)。 我们故意使其类似于 HTTP 通用日志格式,从而所得到的日志可以导入到日志分析工具中。

接下来我们定义一个包含三个 MQTT 服务器的名为 hive_mqupstream 组  (4–9行) 。在我们的测试环境中,他们中的每一个可以通过唯一的端口号由本地主机访问。 zone 指令定义了在所有的 NGINX Plus 工作进程之间共享的内存,以维护负载均衡状态与健康信息。

第 7 段(可获 1.25 积分)

match 块 (11–15行) 定义了用来测试 MQTT 服务器可用性的健康检测。send 指令是一个带有 nginx 健康检测的客户标识的完整 MQTT CONNECT 包的十六进制表示。当健康检测触发时,这会被发送给上游组中定义的每台服务器。相应的 expect 指令描述了服务器必须为 NGINX Plus 返回确定其健康状态的响应。这里,4个字节的 16 进制字符串 20 02 00 00 是一个完整的 MQTT CONNACK 包。该数据的接收表明 MQTT 服务器能够接收新的客户端连接。

第 8 段(可获 1.21 积分)

server 块 (17–25行) 配置 NGINX Plus 如何处理客户端。 NGINX Plus 监听默认的 1883 MQTT 端口,并将所有浏览转发到 hive_mq 上游组 (19行). health_check 指令指定了为上游组执行的健康检测 (以5秒的默认频率) 并使用 mqtt_conn匹配块所定义的检测。

验证配置

为测试基本配置是否工作,我们使用 Mosquitto 客户端向我们的测试环境发布一些测试数据。

第 9 段(可获 1.15 积分)
$ mosquitto_pub -d -h mqtt.example.com -t "topic/test" -m "test123" -i "thing001"
Client thing001 sending CONNECT
Client thing001 received CONNACK
Client thing001 sending PUBLISH (d0, q0, r0, m1, 'topic/test', ... (7 bytes))
Client thing001 sending DISCONNECT
$ tail --lines=1 /var/log/nginx/mqtt_access.log
192.168.91.1 [23/Mar/2017:11:41:56 +0000] TCP 200 23 4 127.0.0.1:18831

访问日志中的记录显示 NGINX Plus 共接收到 23 个字节, 4 个字节被发送给客户端 (CONNACK 包)。同时我们可以看到 MQTT node1 被选中(端口 18831)。正如访问日志中的如下记录所显示的,当我们重复测试时,默认的 Round Robin 负载均衡算法会依次选择 node1node2, 与 node3

第 10 段(可获 0.83 积分)
$ tail --lines=4 /var/log/nginx/mqtt_access.log
192.168.91.1 [23/Mar/2017:11:41:56 +0000] TCP 200 23 4 127.0.0.1:18831
192.168.91.1 [23/Mar/2017:11:42:26 +0000] TCP 200 23 4 127.0.0.1:18832
192.168.91.1 [23/Mar/2017:11:42:27 +0000] TCP 200 23 4 127.0.0.1:18833
192.168.91.1 [23/Mar/2017:11:42:28 +0000] TCP 200 23 4 127.0.0.1:18831

使用nginScript为MQTT负载均衡提供Session持久化

轮询调度负载平衡是一种为跨一组服务器客户端连接的有效的分配机制。然而有几个原因它并不适合MQTT连接。

第 11 段(可获 0.48 积分)

MQTT 服务器通常需要客户端与服务器之间的长时间活动连接,并且在服务器上构建大量的会话状态。不幸的是,IoT 与其所用的 IP 网络的性质意味着连接会断开,强制一些客户端频繁重连。 NGINX Plus 可以使用其 Hash 负载均衡算法基于客户端 IP 地址选择 MQTT 服务器。 简单地将 hash $remote_addr; 添加到上游块启用 会话持久化 从而每次来自指定的客户端 IP 地址的新连接会选择相同的 MQTT 服务器。

第 12 段(可获 1.24 积分)

但是我们不能仅依赖于来自相同 IP 地址的重连,特别是如果他们使用蜂窝网络(例如,GSM 或 LTE )。为确保相同的客户端重连到相同的 MQTT 服务器,我们必须使用 MQTT 客户端标识作为散列算法的键。

基于 MQTT ClientId 的会话持久化

MQTT ClientId 是初始 CONNECT 包的必须元素,这意味着在数据包发送给上游服务器之前可以被 NGINX Plus 访问。 我们使用 nginScript 来解析 CONNECT 包并抽取 ClientId 作为变量,然后该变量可以被 hash 指令用来实现 MQTT 特定的会话持久化。

第 13 段(可获 1.46 积分)

nginScript 是 “NGINX 原生” 的编程式配置语言。它是用于 NGINX 与 NGINX Plus 的独特 JavaScript 实现,特别为服务器端用例与每个请求处理而设计。它有三个关键特点,从而使其适用于 MQTT 的会话持久化实现:

  • nginScript 与 NGINX Plus 处理阶段紧密集成,从而我们可以在客户端包负载均衡到上游组之前进行检查。
  • nginScript 使用内建的 JavaScript 方法进行字符串与数值处理,实现4层协议的高效解析。MQTT CONNECT 的实际解析所需的代码小于20行。
  • nginScript 能够创建 NGINX Plus 配置可用的变量。
第 14 段(可获 1.46 积分)

要了解启用 nginScript 的指令,参看文章结尾的附录 2。

用于会话持久化的 NGINX Plus 配置

用于该用例的 NGINX Plus 配置依然相对简单。下面的配置是 带有主动健康检测的负载均衡 中示例的修改版本,为了简明移除了健康检测。

js_include mqtt.js;
js_set     $mqtt_client_id setClientId;

log_format mqtt '$remote_addr [$time_local] $protocol $status $bytes_received ' 
                '$bytes_sent $upstream_addr $mqtt_client_id'; # Include MQTT ClientId

upstream hive_mq {
    server 127.0.0.1:18831; #node1
    server 127.0.0.1:18832; #node2
    server 127.0.0.1:18833; #node3
    zone tcp_mem 64k;
    hash $mqtt_client_id consistent; # Session persistence keyed against ClientId
}

server {
    listen 1883;
    preread_buffer_size 1k; # Big enough to read CONNECT packet header
    js_preread getClientId; # Parse CONNECT packet for ClientId

    proxy_pass hive_mq;
    proxy_connect_timeout 1s;

    access_log /var/log/nginx/mqtt_access.log mqtt;
    error_log  /var/log/nginx/mqtt_error.log info; # nginScript debug logging
}
第 15 段(可获 0.68 积分)

我们通过 js_include 指令指定 nginScript 代码的位置。 js_set 指令通知 NGINX Plus 在需要计算 $mqtt_client_id 变量的值时调用 setClientId 函数。我们通过将该变量添加到第5行的  mqtt 日志格式 来向访问日志添加更为详细的信息。

我们在第12行通过将 $mqtt_client_id 指定作为键的hash 指令启用会话持久化。注意我们使用 consistent 参数从而如果一个上游服务器失效,其流量共享会均匀分布到其余服务器上而不影响已经在这些服务器上建立的会话。 一致性散列会在我们关于 共享 web 缓存 中的博客中进一步讨论  – 其原则与益处同样适用于这里。

第 16 段(可获 1.48 积分)

js_preread 指令 (18行) 指定在请求处理的预读取阶段执行的 nginScript 函数。预读取阶段会为每个包所触发(双向)并且在代理之前发生,从而 $mqtt_client_id 的值在 upstream 块中是可用的。

用于会话持久化的 nginScript 代码

我们在 mqtt.js 文件中定义了用于获取 MQTT 客户端ID 的 JavaScript,该文件中由 NGINX Plus 配置文件(stream_mqtt_session_persistence.conf) 中的 js_include 指令载入。

第 17 段(可获 1.09 积分)
var client_messages = 1;
var client_id_str = "-";

function getClientId(s) {
    if ( !s.fromUpstream ) {
        if ( s.buffer.toString().length == 0  ) { // Initial calls may
            s.log("No buffer yet");               // contain no data, so
            return s.AGAIN;                       // ask that we get called again
        } else if ( client_messages == 1 ) { // CONNECT is first packet from the client

主要函数, getClientId(), 被声明在第 4 行。为其传递一个表示当前 TCP 会话的对象  s。会话对象有多种,其中一些会被用在该函数中。

第 18 段(可获 0.41 积分)

5–9 行确保当前包是由客户端所接收到的第一个包。后续的客户端消息与服务器端响应被忽略,从而一旦连接被建立,在交通流上不存在额外的开销。

            // CONNECT packet is 1, using upper 4 bits (00010000 to 00011111)
            var packet_type_flags_byte = s.buffer.charCodeAt(0);
            s.log("MQTT packet type+flags = " + packet_type_flags_byte.toString());
            if ( packet_type_flags_byte >= 16 && packet_type_flags_byte < 32 ) {
                // Calculate remaining length with variable encoding scheme
                var multiplier = 1;
                var remaining_len_val = 0;
                var remaining_len_byte;
                for (var remaining_len_pos = 1; remaining_len_pos < 5; remaining_len_pos++ ) {
                    remaining_len_byte = s.buffer.charCodeAt(remaining_len_pos);
                    if ( remaining_len_byte == 0 ) break; // Stop decoding on 0
                    remaining_len_val += (remaining_len_byte & 127) * multiplier;
                    multiplier *= 128;
                }

                // Extract ClientId based on length defined by 2-byte encoding
                var payload_offset = remaining_len_pos + 12; // Skip fixed header
第 19 段(可获 0.51 积分)

这些行检测 MQTT 头部以确保包是 CONNECT 类并确定 MQTT 负载开始的位置。

                var client_id_len_msb = s.buffer.charCodeAt(payload_offset).toString(16);
                var client_id_len_lsb = s.buffer.charCodeAt(payload_offset + 1).toString(16);
                if ( client_id_len_lsb.length < 2 ) client_id_len_lsb = "0" + client_id_len_lsb;
                var client_id_len_int = parseInt(client_id_len_msb + client_id_len_lsb, 16);
                client_id_str = s.buffer.substr(payload_offset + 2, client_id_len_int);
                s.log("ClientId value  = " + client_id_str);
            } else {
                s.log("Received unexpected MQTT packet type+flags: " + packet_type_flags_byte.toString());
            }
        }
        client_messages++;
    }
    return s.OK;
}

function setClientId(s) {
    return client_id_str;
}
第 20 段(可获 0.28 积分)

第1 - 5行提取ClientId载荷,将值存储在JavaScript全局变量client_id_str中。 这个变量将通过setClientIdfunction然后被导出到NGINX配置里(第16–18行).

验证会话持久性

现在我们可以使用Mosquitto客户再次测试会话持久性通过发送一系列的MQTT发布请求发送三个不同ClientId值(使用 ‑i 选项).

$ mosquitto_pub -h mqtt.example.com -t "topic/test" -m "test123" -i "foo"
$ mosquitto_pub -h mqtt.example.com -t "topic/test" -m "test123" -i "bar"
$ mosquitto_pub -h mqtt.example.com -t "topic/test" -m "test123" -i "baz"
$ mosquitto_pub -h mqtt.example.com -t "topic/test" -m "test123" -i "foo"
$ mosquitto_pub -h mqtt.example.com -t "topic/test" -m "test123" -i "foo"
$ mosquitto_pub -h mqtt.example.com -t "topic/test" -m "test123" -i "foo"
$ mosquitto_pub -h mqtt.example.com -t "topic/test" -m "test123" -i "bar"
$ mosquitto_pub -h mqtt.example.com -t "topic/test" -m "test123" -i "bar"
$ mosquitto_pub -h mqtt.example.com -t "topic/test" -m "test123" -i "baz"
$ mosquitto_pub -h mqtt.example.com -t "topic/test" -m "test123" -i "baz"
第 21 段(可获 0.79 积分)

检查访问日志显示 客户端ID foo 总是连接到 node1 (端口 18831), 客户端ID bar 总是连接到 node2 (端口 18832) 而 客户端ID baz 总是连接到 node3 (端口 18833).

$ tail /var/log/nginx/mqtt_access.log
192.168.91.1 [23/Mar/2017:12:24:24 +0000] TCP 200 23 4 127.0.0.1:18831 foo
192.168.91.1 [23/Mar/2017:12:24:28 +0000] TCP 200 23 4 127.0.0.1:18832 bar
192.168.91.1 [23/Mar/2017:12:24:32 +0000] TCP 200 23 4 127.0.0.1:18833 baz
192.168.91.1 [23/Mar/2017:12:24:35 +0000] TCP 200 23 4 127.0.0.1:18831 foo
192.168.91.1 [23/Mar/2017:12:24:37 +0000] TCP 200 23 4 127.0.0.1:18831 foo
192.168.91.1 [23/Mar/2017:12:24:38 +0000] TCP 200 23 4 127.0.0.1:18831 foo
192.168.91.1 [23/Mar/2017:12:24:42 +0000] TCP 200 23 4 127.0.0.1:18832 bar
192.168.91.1 [23/Mar/2017:12:24:44 +0000] TCP 200 23 4 127.0.0.1:18832 bar
192.168.91.1 [23/Mar/2017:12:24:47 +0000] TCP 200 23 4 127.0.0.1:18833 baz
192.168.91.1 [23/Mar/2017:12:24:48 +0000] TCP 200 23 4 127.0.0.1:18833 baz
第 22 段(可获 0.39 积分)

注意,我们也同样获益于出现在访问日志中的 MQTT 客户端ID ,而不论我们是否使用会话持久化或是任何其他的负载均衡算法。

结论

在该系列的第一篇中,我们展示了 NGINX Plus 如何使用主动健康检测来改善 IoT 应用的可用性与可靠性,以及nginScript 如何通过提供一个7层的负载均衡能力--类似 TCP 流量的会话持久化--扩展 NGINX Plus。在第二部分,我们将探索 NGINX Plus 如何通过卸载 TLS 终止与授权客户端来使得你的 IoT 应用更安全。

第 23 段(可获 1.3 积分)

通过组合 nginScript 或是单独使用,NGINX Plus 固有的高性能与高效性使其成为你 IoT 基础设施的理想的软件负载均衡器。

附录

创建测试环境

我们在虚拟机中安装测试环境,从而其是独立可重复的。然而并不存在你不能在真正的,“裸露金属”的服务器上安装测试环境的理由。

安装 NGINX Plus

参看 NGINX Plus 管理指南 中的安装指令。

安装 HiveMQ

可以使用任何 MQTT 服务器,但是该测试环境基于 HiveMQ (由此下载)。在该示例中我们使用 Docker 容器为每个节点在单一主机上安装 HiveMQ 。下面的指令改编自 使用 Docker 部署 HiveMQ .

# Pull base image. The official docker openjdk-8 image is used here.
FROM java:8-jdk

# Copy HiveMQ to container
COPY hivemq.zip /tmp
 
#Install wget and unzip, then download and install HiveMQ.
RUN \
    apt-get install -y wget unzip &&\
    unzip /tmp/hivemq.zip -d /opt/ &&\
    mv /opt/hivemq-* /opt/hivemq
  
# Define working directory.
WORKDIR /opt/hivemq
 
# Define HIVEMQ_HOME variable
ENV HIVEMQ_HOME /opt/hivemq
 
# Expose MQTT port
EXPOSE 1883
 
# Define default command. Here we use HiveMQ's run script.
CMD ["/opt/hivemq/bin/run.sh"]

 

第 24 段(可获 1.5 积分)

 

  1. hivemq.zip 的相同目录下为 HiveMQ 创建一个 Dockerfile 。
  2. 在包含 hivemq.zip 与 Dockerfile 的目录下操作, 创建 Docker image.
    $ docker build -t hivemq:latest .
  3. 创建三个 HiveMQ 节点, 每一个暴露在不同的端口之上.
    $ docker run -p 18831:1883 -d --name node1 hivemq:latest
    ff2c012c595a
    $ docker run -p 18832:1883 -d --name node2 hivemq:latest
    47992b1f4910
    $ docker run -p 18833:1883 -d --name node3 hivemq:latest
    17303b900b64
  4. 检查所有三个 HiveMQ 节点是否运行。 (在下面的示例输出中,为方便阅读,忽略了 COMMANDCREATED, 与 STATUS 列。)
    $ docker ps
    CONTAINER ID  IMAGE          ...  PORTS                     NAMES
    17303b900b64  hivemq:latest  ...  0.0.0.0:18833->1883/tcp   node3
    47992b1f4910  hivemq:latest  ...  0.0.0.0:18832->1883/tcp   node2
    ff2c012c595a  hivemq:latest  ...  0.0.0.0:18831->1883/tcp   node1

安装 Mosquitto

Mosquitto 命令行客户端可以 从项目网站下载. Mac 用户通过下面的命令使用 Homebrew 安装。

$ brew install mosquitto
第 25 段(可获 1.04 积分)

通过向其中一个 Docker image 发送一个简单的发布消息来测试 Mosquitto 客户端与 HiveMQ 安装。

$ mosquitto_pub -d -h mqtt.example.com -t "topic/test" -m "test123" -i "thing001" -p 18831
Client thing001 sending CONNECT
Client thing001 received CONNACK
Client thing001 sending PUBLISH (d0, q0, r0, m1, 'topic/test', ... (7 bytes))
Client thing001 sending DISCONNECT

为 NGINX 与 NGINX Plus 启用 nginScript

为 NGINX Plus 载入 nginScript

对于 NGINX Plus 订阅者,nginScript 可以作为免费 动态模块 而获得 (对于开源 NGINX, 参看下面的 为开源 NGINX 载入 nginScript )。

第 26 段(可获 0.69 积分)
  1. 通过由 NGINX Plus 安装获取模块本身。
    • 对于 Ubuntu 与 Debian 系统:
      $ sudo apt‑get install nginx-plus-module-njs
    • 对于 RedHat, CentOS, 与 Oracle Linux 系统:
      $ sudo yum install nginx-plus-module-njs
    • nginx.conf 配置文件的顶级语境 ("main") (而不是 httpstream 语境) 中包含 load_module 来启用模块。下面的示例为 HTTP 流量载入 nginScript 模块。
      load_module modules/ngx_http_js_module.so;
    • 重新载入 NGINX Plus 来将 nginScript 模块装载到运行实例中。
      $ sudo nginx -s reload

为开源 NGINX 载入 nginScript

如果你的系统被配置用来使用官方 开源 NGINX 的预编译包 而你的安装版本为 1.9.11 或更高, 那么你可以为你的平台将 nginScript 安装为预编译包。

第 27 段(可获 1.45 积分)
  1. 安装预编译包。
    • 对于 Ubuntu 与 Debian 系统:
      $ sudo apt-get install nginx-module-njs
    • 对于 RedHat, CentOS, 与 Oracle Linux 系统:
      $ sudo yum install nginx-module-njs
  2. 通过在 nginx.conf 配置文件中的顶级语境 ("main") 中包含 load_module 指令来启用模块(而不是在 httpstream 语境中)。下面的示例为 HTTP 流量装载 nginScript 模块。
    load_module modules/ngx_http_js_module.so;
  3. 重新载入 NGINX Plus 以将 nginScript 模块装载到运行实例中。
    $ sudo nginx -s reload

将 nginScript 编译为开源 NGINX 的动态模块

如果你喜欢由源码来编译 NGINX 模块:

  1. 遵循开源仓库 中的 这些指令 来构建 nginScript 模块。
  2. 将二进制模块 (ngx_http_js_module.so) 拷贝到 NGINX 根目录 (通常为 /etc/nginx/modules) 下的 modules 子目录。
  3. 执行步骤 2 与步骤 3 为开源 NGINX 载入 nginScript 。
第 28 段(可获 1.66 积分)

文章评论