BT

如何利用碎片时间提升技术认知与能力? 点击获取答案

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

| 作者 Amjad Afanah 关注 0 他的粉丝 ,译者 张卫滨 关注 13 他的粉丝 发布于 2016年6月12日. 估计阅读时间: 38 分钟 | 如何结合区块链技术,帮助企业降本增效?让我们深度了解几个成功的案例。

本文所阐述的解决方案能够在任意的云或虚拟化平台中自动构建、部署和管理基于Docker的 Java微服务应用。我们扩展了一个已有的项目,这就是Chris Richardson针对事件溯源、CQRSDocker所创建的转账样例,我们将会为这个项目引入自动构建的功能。我们的这个项目为每个微服务都创建了一个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 HubQuay仓库中,那么可以使用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 AfanahDCHQ的联合创始人,这是一个Docker管理解决方案,关注于企业级应用的建模、部署、服务发现以及生命周期管理。在创建DCHQ之前,Amjad曾经在Oracle和VMware担任高级产品管理职位,当时他推动着云系统管理和应用部署&管理方面的战略性产品。

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

评价本文

专业度
风格

您好,朋友!

您需要 注册一个InfoQ账号 或者 才能进行评论。在您完成注册后还需要进行一些设置。

获得来自InfoQ的更多体验。

告诉我们您的想法

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我
社区评论

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我

讨论

登陆InfoQ,与你最关心的话题互动。


找回密码....

Follow

关注你最喜爱的话题和作者

快速浏览网站内你所感兴趣话题的精选内容。

Like

内容自由定制

选择想要阅读的主题和喜爱的作者定制自己的新闻源。

Notifications

获取更新

设置通知机制以获取内容更新对您而言是否重要

BT