使用 DCHQ 自动部署和管理基于 Docker 的云 / 虚拟化环境 Java 微服务

阅读数:1555 2016 年 6 月 11 日 04:28

本文所阐述的解决方案能够在任意的云或虚拟化平台中自动构建、部署和管理基于 Docker 的 Java 微服务应用。我们扩展了一个已有的项目,这就是 Chris Richardson 针对事件溯源、 CQRS Docker 所创建的转账样例,我们将会为这个项目引入自动构建的功能。我们的这个项目为每个微服务都创建了一个Dockerfile,同时也提供了统一的前端页面,这个前端功能会用到所有的微服务,它可以运行在任意的Web 服务器上。我们将会使用 Nginx 作为 Web 服务器,并将前端的 JavaScript 代码放到默认的 /usr/share/nginx/html/ 目录中。我们的前端会暴露如下的功能:

  • 基于一个初始的余额,创建新账户。
  • 查询某个账户,得到其余额。
  • 从一个账户到另一个账户进行转账。

我们所创建的转账应用将会作为构建和部署微服务的样例,这些微服务包括了事件溯源、CQRS 和 Docker。这个基于微服务的应用架构是高度可扩展和高度可重用的,用到了 polyglot 持久化(polyglot persistence)、事件溯源(event sourcing,ES)以及命令与查询的责任分离(command query responsibility segregation,CQRS)。微服务应用是由松耦合的组件所组成的,它们会使用事件来实现通信。这些组件可以部署为独立的服务,也可以将其打包为一个单体应用,以便于简化开发和测试。在这个项目中,我们会关注于前一种方式的自动化——也就是,使用独立的服务来部署应用,这些服务会运行在 Docker 容器上。

我们的目标就是在 13 个不同的云和虚拟化平台中(包括 vSphere、OpenStack、AWS、Rackspace、Microsoft Azure、Google Compute Engine、DigitalOcean、IBM SoftLayer 等)运行和管理本项目中的事件溯源 Docker Java 微服务应用模板。我们建议你通过如下的方式之一来跟随我们的步骤:

背景

对企业级 Java 应用进行容器化是一项具有挑战性的事情,这在很大程度上是因为现有的应用组合框架并没有解决掉复杂的依赖、外部集成以及添加服务实例之后(Post-Provision)的自动扩展流程等问题。另外,容器生存周期的短暂性会强迫开发人员生成新的容器,并且在每次版本更新的时候,重新创建复杂的依赖和外部集成。

DCHQ 可以通过托管版本和 on-premise 版本进行使用,它解决了上述的挑战并简化了企业级 Java 应用的容器化,DCHQ 是通过一个高级应用组合框架来实现的,这个框架扩展了 Docker Compose ,支持跨镜像的环境变量绑定,可扩展的 BASH 脚本插件,这个脚本可以在请求的时候执行,也可以在提供实例之后执行,同时,为了实现高可用性,还提供了应用集群的功能,这个集群能够跨多个主机和 region,支持自动扩展。

在应用添加进来之后,用户就可以:

  • 监控运行时容器的 CPU、内存以及 I/O,
  • 获取提醒和告警,
  • 访问应用的备份、自动化的扩展 / 伸缩流程以及插件执行流程,从而更新运行时容器。

除此之外,内置的工作流程有助于使用 Jenkins 实现持续集成(稍后将会支持更多的构建服务器),这样允许开发人员刷新正在运行应用的 Java WAR 文件,而不会打乱已有的依赖和集成。

在本文作者的其他博客文章中,我们已经阐述了更为传统的或者说典型的 Java 应用(如命名目录、Pizza 商店以及电影商店应用)如何在基于 Docker 的多层应用栈上实现端到端的部署自动化,这些技术栈涵盖了 13 种不同的云和虚拟化平台。

在当前的这个项目中,我们将会聚焦于微服务架构,它根本不需要应用服务器。每个微服务都运行在特别轻量级的 Java 容器中。我们会构建统一的前端,它会发送 REST API 调用到相关的服务,从而执行特定的任务(如创建账户、查询账户或者将钱从一个账户转到另一个账户)。微服务的主要优势之一(与传统的单体应用相比)就是这些模块化的服务能够很容易地进行替换和扩展,而不需要对其他微服务做出修改。按照这种方式,会消除单点故障,也会让开发人员更容易地将精力投入到整体的项目中。

在这个项目中,我们将会提供 step-by-step 的指南,指导读者在不同的云 / 虚拟化基础设施下部署和管理该 Java 应用。

我们将会执行如下的步骤,稍后将会进行详细介绍:

  • 为 Event Store 获取凭证
  • 添加一个 patch 并构建 JAR 文件
  • 在本项目中,借助 DCHQ,通过 Dockerfiles 自动构建 Docker 镜像
  • 构建基于 YAML 的应用模板,它可以在任意 Linux 主机上重用,从而能够到处运行
  • 在任意的云环境中,提供和自动扩展底层的基础设施(在本文中,使用 Rackspace 作为样例)
  • 在 Rackspace 集群中部署多层的 Java 应用
  • 监控运行时容器的 CPU、内存和 I/O
  • 借助 Jenkins,启用持续交付的工作流程,当构建触发时,会更新运行中微服务的 JAR 文件

接下来,我们将会详细介绍每个步骤:

为 Event Store 获取凭证

为了单独的运行微服务,我们需要为 Event Store获取凭证。

将为EVENTUATE_API_KEY_IDEVENTUATE_API_KEY_SECRET获取到的值复制并粘贴到事件溯源 Docker Java Microservices 应用的模板中

添加一个 patch 并构建 JAR 文件

Docker 镜像中所使用的 JAR 文件是通过该项目构建的。

所有的 JAR 文件都是在 2015 年 12 月 27 日构建的,并且已经嵌入到了 Docker 镜像中,可以查看该地址

在构建 JAR 文件之前,将 CORSFilter.java 文件复制到“event-sourcing-examples/java-spring/common-web/src/main/java/net/chrisrichardson/eventstore/javaexamples/banking/web/util”目录中,我们可以这样执行:

`./gradlew assemble.
git clone https://github.com/cer/event-sourcing-examples.git

wget https://github.com/dchqinc/event-sourcing-microservices/raw/master/patch/CORSFilter.java -O /event-sourcing-examples/java-spring/common-web/src/main/java/net/chrisrichardson/eventstore/javaexamples/banking/web/util/CORSFilter.java

cd /event-sourcing-examples/java-spring

./gradlew assemble` 

在本项目中,借助 DCHQ,通过 Dockerfiles 自动构建 Docker 镜像

该项目中的所有镜像构建好了,并推送到了 DCHQ 公开的 Docker Hub 仓库中,便于读者进行引用。如下就是在当前的应用模板中要使用的自定义镜像:

  • dchq/nginx-microservices:latest
  • dchq/accounts-command-side-service
  • dchq/transactions-command-side-service
  • dchq/transactions-command-side-service-

如果要构建镜像并将将其推送到自己的 Docker Hub Quay 仓库中,那么可以使用 DCHQ。如下就是这些镜像所使用的 GitHub 项目:

在登录进 DCHQ 之后(托管的 DCHQ.io 版和 On-Premise 版本均一样),我们可以导航到Automate > Image Build,然后点击+按钮来创建新的Dockerfile(Git/GitHub/BitBucket)镜像构建。

为必填域提供如下的值:

  • Git URL
  • Git Branch——这个域是可选的——但是,你可以使用它来指定 GitHub 项目的一个分支。默认的分支是 master。
  • Git Credentials——在 DCHQ 中,我们可以将凭证安全存储到一个私有的 GitHub repository 里面,导航到Manage> Cloud Providers & Repos,然后点击+来选择Credentials
  • Cluster——构建 Docker 镜像是通过 DCHQ agent 来组织的。所以,我们需要选择一个集群,以便于 agent 用它来执行 Docker 镜像的构建。如果目前还没有创建集群的话,请参考该章节,要么注册一个运行时的主机,要么自动提供新的虚拟化基础设施。
  • Push to Registry——将新创建的镜像推送到公有或私有的仓库中,这些仓库可以位于 Docker Hub 或 Quay 上。要注册 Docker Hub 或 Quay 账号,可以导航至Manage > Cloud Providers & Repos,然后点击+来选择Docker Registries
  • Repository——这是镜像要推送到的 repository 名称。例如,我们的镜像将会推送至dchq/php-example:latest
  • Tag——这是你希望为新镜像所添加的标签名。在 DCHQ 中,支持的标签名包括:
    • {{date}}——格式化的日期
    • {{timestamp}}——完整的时间戳
  • Cron Expression——使用内置的 cron 表达式调度 Docker 镜像的构建。这有助于为用户提供每日或每夜构建的功能。

在必填域完成之后,点击Save

这样,我们就可以点击Play按钮,按需构建 Docker 镜像了。

构建基于 YAML 的应用模板,它可以在任何 Linux 主机上重用,从而能够到处运行

在登录进 DCHQ 之后(托管的 DCHQ.io 版和 On-Premise 版本均一样),用户可以导航至Manage >App/Machine,然后点击+按钮来创建新的Docker Compose模板。关于如何创建 Docker Compose 应用模板,可以参考这里的详细文档。

我们已经使用上一步中所构建的 Docker 镜像创建了一个应用模板。这个模板包含了如下的组件:

  • Nginx——为该微服务应用托管统一的前端
  • 账户创建、账户查询以及余额转账的微服务——这些微服务是由最初的项目构建而来。通过复制“event-sourcing-examples/java-spring/common-web/src/main/java/net/chrisrichardson/eventstore/javaexamples/banking/web/util”目录下的 CORSFilter.java,为其添加了一个 patch。
  • Mongo——用于数据库

在请求时和提供实例后配置 Web 服务器的插件

在应用模板中,你可能会注意到 Nginx 容器在请求时(request time)会触发一个 BASH 脚本插件,以便于配置容器。这个插件也可以在新增服务实例之后执行。

我们可以通过导航至Manage > Plugins来创建这些插件。在提供完 BASH 脚本之后,DCHQ agent 将会在容器内执行该脚本。我们还可以指定参数,这些参数能够在请求时或提供实例后进行重写。所有以$符号作为前缀的内容均会被视为参数——例如,$file_url可以作为参数,允许开发人员指定 WAR 文件的下载 URL。如果用户想要在一个运行时的容器中刷新 Java WAR 文件的话,这个参数可以在请求时和提供实例后进行重写。

在定义基于 YAML 的应用模板时,我们需要提供插件 ID。例如,要触发 Nginx 的 BASH 脚本插件,我们需要按照如下的方式来引用插件 ID:

`nginx:
  image: dchq/nginx-microservices:latest
  publish_all: true
  mem_min: 50m
  host: host1
  plugins:
    - !plugin
      id: Gl5Hi
      restart: true
      lifecycle: on_create
      arguments:
        - ACCOUNT_CMD_IP={{accountscommandside | ip}}
        - ACCOUNT_CMD_PORT={{accountscommandside | port_8080}}
        - ACCOUNT_TRANSFER_IP={{transactionscommandside | ip}}
        - ACCOUNT_TRANSFER_PORT={{transactionscommandside | port_8080}}
        - ACCOUNT_QUERY_IP={{accountsqueryside | ip}}
        - ACCOUNT_QUERY_PORT={{accountsqueryside | port_8080}}` 

在这个样例中,Nginx会调用一个 BASH 脚本插件,它会动态(或在请求时)将微服务容器的 IP 和端口号注入到/usr/share/nginx/html/js/app.js文件中。插件的 ID 是Gl5Hi

通过插件的生命周期阶段,实现服务发现功能

通过使用插件中的lifecycle参数,我们能够在指定的精确阶段或事件发生之时,执行该插件。如果没有指定lifecycle的话,那么按照默认的情况,插件将会在on_create之时执行。你可以参考这里的详细文档,以了解如何搭建 Docker 服务的发现功能。如下是支持的生命周期阶段:

  • on_create——当创建容器的时候执行插件
  • on_start——在容器启动的时候执行插件
  • on_stop——当容器停止的时候执行插件
  • on_destroy——在销毁容器之前执行插件
  • post_create——在容器创建和运行之后执行插件
  • post_start[:Node]——在另外一个容器启动之后执行插件
  • post_stop[:Node]——在另外一个容器停止之后执行插件
  • post_destroy[:Node]——在另外一个容器销毁之后执行插件
  • post_scale_out[:Node]——在另外一个容器集群横向扩展之后执行插件
  • post_scale_in[:Node]——在另外一个容器集群伸缩之后执行插件

通过 cluster_size 和 host 参数实现跨多主机的 HA 部署

我们将会看到通过cluster_size参数能够指定要搭建容器的数量(它们具有相同的应用依赖)。

host参数能够用来指定想要部署容器的主机。当创建集群的时候,如果选择了 Weave 作为网络层,那么就可以确保应用服务器集群的高可用性,它们会跨多个主机(或 region),允许我们遵循关联性规则(affinity rule),比如说能够将数据库运行在不同的主机上。如下是 host 参数所支持的值:

  • 主机 1, 主机 2, 主机 3等——在数据中心(或集群)任意选择一个主机来进行容器部署。
  • IP 地址 1,IP 地址 2等——允许用户指定实际的 IP 地址,用于容器的部署
  • 主机名 1, 主机名 2等——允许用户指定实际的主机名,用于容器的部署
  • 通配符(如“db-”或“app-srv-”)——指定主机名所对应的通配符

跨镜像的环境变量绑定

除此之外,通过引用其他镜像的环境变量,用户可以创建跨镜像的环境变量绑定。在本例中,我们也使用了多个这样的绑定——包括 ACCOUNT_CMD_IP={{accountscommandside | ip}}——Account Creation 微服务容器的 IP 是在请求时动态解析的,它还用来确保 Nginx 能够建立与该微服务的连接。

如下是所支持环境变量值列表:

  • {{alphanumeric | 8}}——创建一个随机的由 8 个字符所组成的字母数字字符串。在创建随机密码时,它最为有用。
  • {{Image Name | ip}}——允许我们输入某个容器的主机 IP 地址,将其作为环境变量的值。在中间件层与数据库建立连接时,这种方式最为有用。
  • {{Image Name | container_ip}}——允许将容器的名称作为环境变量的值。在中间层与数据库建立安全连接时(不用暴露数据库的端口),这种方式最为有用。
  • {{Image Name | container_private_ip}}——允许将容器的内部 IP 作为环境变量的值。在中间层与数据库建立安全连接时(不用暴露数据库的端口),这种方式最为有用。
  • {{Image Name | port_Port Number}}——允许将容器的端口作为环境变量的值。在中间件层与数据库建立连接时,这种方式最为有用。在这种场景下,所指定的需要是内部端口号——也就是说,不是分配给容器的外部端口。例如,{{PostgreSQL | port_5432}}将会转换为实际的外部端口,从而允许中间件与数据库建立连接。
  • {{Image Name | Environment Variable Name}}——允许将某个镜像的环境变量值作为另一个镜像的环境变量。这里的使用场景就无穷无尽了——因为大多数的多层应用都会有跨镜像的依赖。

事件溯源的 Docker Java 微服务

`nginx:
  image: dchq/nginx-microservices:latest
  publish_all: true
  mem_min: 50m
  host: host1
  plugins:
    - !plugin
      id: Gl5Hi
      restart: true
      lifecycle: on_create
      arguments:
        - ACCOUNT_CMD_IP={{accountscommandside | ip}}
        - ACCOUNT_CMD_PORT={{accountscommandside | port_8080}}
        - ACCOUNT_TRANSFER_IP={{transactionscommandside | ip}}
        - ACCOUNT_TRANSFER_PORT={{transactionscommandside | port_8080}}
        - ACCOUNT_QUERY_IP={{accountsqueryside | ip}}
        - ACCOUNT_QUERY_PORT={{accountsqueryside | port_8080}}

accountscommandside:
  image: dchq/accounts-command-side-service
  mem_min: 300m
  cluster_size: 1
  host: host1
  publish_all: true
  environment:
    - EVENTUATE_API_KEY_ID=
    - EVENTUATE_API_KEY_SECRET=

transactionscommandside:
  image: dchq/transactions-command-side-service
  mem_min: 300m
  cluster_size: 1
  host: host1
  publish_all: true
  environment:
    - EVENTUATE_API_KEY_ID=
    - EVENTUATE_API_KEY_SECRET=

accountsqueryside:
  image: dchq/accounts-query-side-service
  mem_min: 300m
  cluster_size: 1
  host: host1
  publish_all: true
  environment:
    - EVENTUATE_API_KEY_ID=
    - EVENTUATE_API_KEY_SECRET=
    - SPRING_DATA_MONGODB_URI=mongodb://{{mongodb | container_private_ip}}/mydb

mongodb:
  image: mongo:3.0.4
  host: host1` 

在任意的云环境中,提供和自动扩展底层的基础设施

应用保存之后,我们就可以注册一个云提供商,从而自动化地在集群中提供服务实例和横向扩展,它支持 12 中不同的云终端,包括 VMware vSphere、OpenStack、 CloudStack、Amazon Web Services、Rackspace、Microsoft Azure、DigitalOcean、IBM SoftLayer、Google Compute Engine 等。

比如说,要将 Rackspace 注册为云提供商,导航至Manage > Cloud Providers and Repos,并点击+按钮来选择Rackspace。这里需要提供 Rackspace API Key——在 Rackspace Cloud Control Panel 的 Account Settings 区域可以得到这个 Key。

这样,我们就可以基于一个自动扩展的策略来创建集群,该策略会自动生成新的云服务器。这项任务可以通过导航至Manage > Clusters页面,然后点击+按钮完成。我们可以选择一个基于处理能力的部署策略(capacity-based placement policy),然后选择Weave作为网络层,这样在一个集群内就能实现多主机间安全的、密码保护的跨容器通信。例如,如果使用Auto-Scale Policy的话,我们可能会将 VM(或云服务器)的最大数量设置为 10。

这样在新创建的集群上,我们就能提供一定数量的云服务器了,这可以通过基于 UI 的工作流程来实现,也可以定义一个简单的基于 YAML 的 Machine Compose 模板,Self-Service Library 将会请求使用该模板。

基于 UI 的工作流程——如果要请求 Rackspace 云服务器的话,我们可以导航至Manage > Machines,然后点击+按钮来选择Rackspace。在选择完云提供商之后,接下来就可以选择 region、大小以及所需的镜像。在 Rackspace 云服务器上,为了满足一些端口方面的需求,有些端口默认是打开的(如 32000-59000 留给 Docker,6783 留给 Weave 而 5672 留给 RabbitMQ)。然后,可以选择集群并指定云服务器的数量。

基于 YAML 的 Machine Compose 模板——我们可以首先为 Rackspace 创建一个 Machine Compose 模板,这需要通过导航至Manage > App/Machine,然后选择Machine Compose来实现。

如下就是请求 4GB 云服务器的模板:

Medium:
  region: IAD
  description: Rackspace small instance
  instanceType: general1-4
  image: IAD/5ed162cc-b4eb-4371-b24a-a0ae73376c73
  count: 1` 

Machine Compose 模板所支持的参数概述如下:

  • description:blueprint/ 模板的描述
  • instanceType:云提供商特定的值(如 general1-4)
  • region:云提供商特定的值(如 IAD)
  • image:强制要求——镜像 ID/ 名称(如 IAD/5ed162cc-b4eb-4371-b24a-a0ae73376c73 或 vSphere VM 模板的名称)的全限定名
  • username:可选——只针对 vSphere VM 模板的用户名
  • password:可选——只针对 vSphere VM 模板的加密后的密码。可以使用端点来对密码进行加密
  • network:可选——云提供商特定的值(如 default)
  • securityGroup:云提供商特定的值(如 dchq-security-group)
  • keyPair:云提供商特定的值(如私钥)
  • openPorts:可选——逗号分隔的端口值
  • count:VM 的总数,默认为 1

在 Machine Compose 模板保存之后,就可以通过 Self-Service Library来请求该机器。我们可以点击Customize,然后点击Cloud ProviderCluster来请求这些 Rackspace 云服务器。

在 Rackspace 集群上部署多层的 Java 应用

在提供了云服务器之后,我们就可以在这些新的云服务器上部署多层的、基于 Docker 的 Java 应用。这可以通过导航至 Self-Service Library 并点击 Customize 来实现。

选择一个环境标签(如 DEV 或 QE)和你所创建的 Rackspace 集群,然后点击 Run。

在浏览器终端中访问运行中的容器

在 Live Apps 页面中,容器名称的旁边会有一个命令行的提示图标。这允许用户使用安全的通信协议,借助 agent 的消息队列进入到容器中。通过 Tenant Admin,可以定义白名单的命令,从而保证用户不会对正在运行中的容器做出有损害的变更。

例如,对于 Nginx 容器来说,我们使用命令提示来确保 app.js 文件包含了合适的 IP 和端口,从而能够访问 Docker Java 微服务。

在这个截图中,我们使用浏览器中的终端来展现 Nginx 容器里面/usr/share/nginx/html/js/app.js 文件的内容,可以看到 Docker Java 微服务的 IP 和端口已经借助 DCHQ 的插件框架正确注入到了文件之中。

监控运行时容器的 CPU、内存和 I/O

应用启动并运行之后,开发人员就能监控运行时容器的 CPU、内存和 I/O,从而在它们超出某个预定义的阈值时得到告警信息。在执行功能性和负载测试的时候,这会特别有用。

我们还可以进行历史数据的监控分析,然后对容器更新或构建部署的相关问题进行分析。这可以通过点击Stats,然后选择自定义的日期范围来查看 CPU、内存和 I/O 的历史状况。

在 Jenkins 触发构建时,替换容器或更新运行中应用的 JAR 文件来实现持续交付

“不可变(immutable)”的容器模型通常是最佳实践,这需要重新构建包含应用代码的 Docker 镜像,并且在每次更新的时候,生成新的容器。DCHQ 提供了自动构建的特性,允许开发人员通过 Dockerfiles 或包含 Dockerfiles 的私有 GitHub 项目自动化地创建 Docker 镜像。然后,这些镜像会被推送到 Docker Private Registry、Docker Hub 或 Quay 上已注册的私有或公开 repositories 中。

我们可以用新容器自动化地“替换”正在运行中的容器,这些新容器是通过推送到 Docker registry 的最新镜像生成的。这个过程可以请求式地执行(on-demand),也可以在 Docker registry 上探测到新镜像时自动化地执行。要使用包含最新 JAR 文件的新容器替换 Docker Java 微服务容器时,用户只需点击Actions菜单并点击Replace即可。然后,用户输入镜像的名称,这个镜像就是用来生成新容器的,这个容器会按照相同的应用依赖替换掉正在运行的容器。另外一种方案,用户可以为容器替换指定一个触发器——这个触发器基于简单的 CRON 的表达式(如预先定义的调度)或基于推送到 Docker registry 上的最新镜像。

很多开发人员可能会希望使用最新的 Java JAR 文件来更新运行中的容器。要实现这一点,DCHQ 允许开发人员借助 Jenkins 启用自动构建的流程。在运行的应用上点击Actions菜单,然后选择Continuous Delivery。我们可以选择已经在 DCHQ 上注册的 Jenkins 实例,Jenkins 的实际任务就是生成最新的 JAR 文件,一个 BASH 脚本插件跟踪到这个构建过程并将其部署到一个运行中的应用服务器上。这个策略保存之后,每当构建触发之后,DCHQ 都会从 Jenkins 上获取最新的 JAR 文件,并将其部署到运行中的应用服务器上。

这样的话,在 DEV/TEST 环境下,开发人员始终能够将最新的 JAR 文件部署到运行中的容器里面。

结论

容器化企业级 Java 应用的主要挑战在于已有的应用组合框架并没有解决掉复杂的依赖、服务发现以及添加服务实例之后(Post-Provision)的自动扩展流程等问题。

DCHQ 可以通过托管版本和 On-Premise 版本进行使用,它解决了上述的所有挑战并简化了企业级 Java 应用的容器化,DCHQ 是通过一个高级应用组合框架来实现的,这个框架支持跨镜像的环境变量绑定,可扩展的 BASH 脚本插件,这个脚本可以在应用部署的不同生命周期阶段调用,同时,为了实现高可用性,还提供了应用集群的功能,这个集群能够跨多个主机和 region,支持自动扩展。

你可以在 http://DCHQ.io 免费注册或下载 DCHQ On-Premise 版本,使用内置的多层 Java 应用模板以及应用生命周期管理功能,如监控、容器更新、扩展 / 伸缩以及持续交付。

关于作者

Amjad Afanah DCHQ 的联合创始人,这是一个 Docker 管理解决方案,关注于企业级应用的建模、部署、服务发现以及生命周期管理。在创建 DCHQ 之前,Amjad 曾经在 Oracle 和 VMware 担任高级产品管理职位,当时他推动着云系统管理和应用部署 & 管理方面的战略性产品。

查看英文原文: Automate Deployment & Management of Docker Cloud/Virtual Java Microservices with DCHQ

收藏

评论

微博

用户头像
发表评论

注册/登录 InfoQ 发表评论