标签 API 下的文章

现代企业应用架构-使用Docker CaaS交付敏捷的、可移植的、受控的应用

年初,火得发烫的独角兽IT公司Docker发布了一款新的企业级产品 Docker Datacenter (简称:DDC)。作 为拥有原生Docker容器技术的公司,其每个市场动作都会让轻量级容器生态圈内的公司不敢小觑。而要揣度Docker对商业改变的理解、对容器 技术栈应用的理解以及对新产品和服务在生态圈中的定位,就有必要对Docker的这款产品做一些比较深刻的了解。而其技术白皮书 恰是我们了解 Docker该产品的入口。这里我就基于自己对容器相关技术栈的粗浅理解,翻译一下这篇篇幅不长的技术白皮书,希望能给大家带来些许帮助。

标题:现代企业应用架构-使用Docker CaaS交付敏捷的、可移植的、受控的应用

译文全文如下:

摘要

开发人员不接受被锁住的平台。就像《金发小女孩和三只熊》 故事那样,开发人员们一直在为其开发环境寻找一种可以在自由和约束之间拥 有最佳平衡的权力。在这个过程中,他们发现“平台即服务”(PaaS)模型层次太高、过于抽象以及约束过多,并且为了实现一个完全锁定的、黑盒的 环境而牺牲了灵活性;同时,他们也发现“基础设施即服务”(IaaS)模型提供的各自的容器服务也是不够的,因为那种服务仅驻留在各自的基础设施 中,缺乏远见。在寻求适当方案的过程中,一些组织开始提供基于Docker的“容器即服务”(CaaS)的环境,这种模型为开发团队提供了敏捷 性;为运维团队提供了控制力;为应用程序提供了跨基础设施的可移植性 — 从本地数据中心到公有云,横跨诸多网络和存储设备供应商。

Docker平台为基础设施无关的CaaS模型提供了一套集成套件。使用这个方案,IT运维团队既可以对基础设施,也可以对基础应用内容进行安全 保护、配置和管理;同时开发人员也能够以自助的方式来构建和部署他们的应用。

在本白皮书中,我们将讨论新软件模型的驱动力,Docker平台的能力,细化CaaS的需求,以及详细说明在构建、交付(运输)和运行应用程序过 程中解决核心问题的重要性。

重要结论包括:

• 云、数据和微服务是如何改变商业的
• 理解Docker的发展历程
• Docker CaaS模型的能力与优势

一、通过软件改变商业

运行成品软件的私有数据中心以及一年更新一次的巨大单一代码库的时代已经离我们远去了。一切都在变化。不管是迁移到云上,在云间移植,用现代化的 方法改造遗留程序,还是构建新的应用和数据结构,我们想要的结果都是相同的 – 速度。你动作的越快,你的公司将会越成功。

软件是定义你的公司的关键IP(知识产权),即便你的公司实际出售的商品可能只是一件T恤、一辆车或复利(compounding interest)。软件就是你如何接洽客户,如何吸引新用户,如何理解他们的数据,如何推广你的产品或服务以及如何处理他们的订单。

要做好这些,当今的软件正趋向定制化。为一个非常具体的工作而设计的软件片段被称为微服务(microservice)。微服务的设计目标是让 每一个由必要组件构建出来的服务在适当类型的底层基础设施资源上运行一个特定的工作(job)。接下来,这些服务松耦合在一起,可以随时被修改, 无需担心服 务运行的先后次序。

这种方法,虽然对持续改进十分有利,但在达成最终结果的过程中也提出了许多挑战。首先,它创建了一个新的、不断膨胀的服务、依赖和基础设施矩阵, 让它自身很难于管理。此外,它没有考虑到眼前大量已经存在的遗留程序,完全异构的应用程序栈以及实际中必须保证运行起来的进程。

二、Docker的发展历程以及AND的力量

2013年,Docker以具备构建、交付、到处运行的应用容器而出现在大众视野当中。与今天集装箱的运输类似,软件容器就是一个软件的标准单 元,不管容器内存放的代码和依赖是什么,容器外部看起来都相同。这使得开发人员和系统管理员可以跨基础设施和各种各样环境传输容器,而无需做任何 修改和考虑不同环境下的不同配置。Docker的历程就从此时开始了。

敏捷性: Docker的速度和简洁让Docker一经推出便大受开发者欢迎,同时也使得其开源项目的热度以流星般速度蹿升。现在开发者能很容易地将软件以及其依赖 打包到一个容器中。开发者可以使用任何语言、版本和工具,因为这些都被打包到一个容器中,容器将所有异质性标准化了,并且无需付出任何代价。

可移植性: Docker技术的本质让那批开发者意识到他们的应用容器现在可移植了,而且是以在以前不可能的方式。他们可以将应用从开发环境直接交付到测试和产品环境 且代码总是按设计那样正常工作。环境中的任何差异都不会影响到容器里面的应用。应用也无需修改就可以正常工作在生产环境中。这同样也是IT运维团 队的一个福音,因为现在他们可以跨数据中心迁移应用来避免厂商的平台锁定了。

控制: 当应用程序沿着通往生产环境的生命周期前进时,关于安全性、可管理性以及伸缩性等新问题需要进一步得到解答。Docker标准化了你的环境,同时维护着你 的业务所需的异质性。Docker提供了设置适当控制级别的能力以及维护服务级别、性能以及监管的灵活性。IT运维组能够通过供应、安全加固、监 控和伸缩基础设施和应用来保持峰值服务水平。没有两个程序或业务是一样的,Docker允许你决定如何去控制你的应用环境。

Docker成长历程的核心是AND的力量。Docker是唯一一个可以跨应用生命周期所有阶段,为开发者和IT运维团队在提供敏捷性、可移植性 和控制的方案。从这些核心原则来看,CaaS的脱颖而出正是由于由其构建的新应用又好又快。

三、Docker Containers as a Service(CaaS)

容器即服务(CaaS)是什么?它是基于基础设施和内容的一个IT受控的、安全的应用环境,利用它开发人员可以以自助的方式构建和部署应用。

img{512x368}

在上面的CaaS图示中,开发和IT运维团队通过registry相互协作。registry服务用于维护一个安全的、经过签名的映像仓库。左边 的开发者通过registry服务可以将软件拉(pull)到本地,按自己的步伐构建软件。当软件通过集成测试,开发者将其内容推回(push back)registry以保存最新版本。部署步骤因内部过程的不同而异,既可以通过工具自动进行,也可以是人工部署。

上图中右侧的IT运维组为生产环境基础设施管理着不同供应商的合同,诸如:计算、网络和存储。这些团队负责提供应用所需的计算资源,使用 Docker Universal Control Plane随时随地监控集群和应用。他们能在云间迁移应用,或伸缩服务来维持峰值服务水平。

四、关键特性和考量

Docker CaaS为组织提供了一套框架用于统一他们环境中的各种系统、语言和工具,并为业务提供所需的控制、安全或特权级别。由于是一种支持全部Docker API的Docker原生方案,Docker CaaS能够无缝地将应用从本地开发环境部署到生产环境,而无需改变代码或简化部署周期。

以下特性组成了组织应用环境的最低需求。在这个范式中,开发和运维团队被授权使用各自最佳的工具,而无需担心对系统、其他人的工作流或锁定状态造 成破坏。

1、开发者和运维的需求。 许多工具仅能解决针对一个团队的功能需求,但CaaS打破了持续改进的周期。为了获得从开发到生产环境运行的真正加速,你需要在一个连续周期内同时满足两类用户的需求。Docker为每个团队都提供了独特的能力,同时还提供了横跨整个平台的一致的API,保证了从一个团队到另外一个团队的无缝过渡。

2、应用程序生命周期的所有阶段。 从持续集成到持续交付以及开发运维(devops),这些实践都是为了消除瀑布开发方法以及其带来的滞后的周期。通过给开发和运维团队提供工具,Docker可以无缝的支持应用从构建、测试到部署到生产环境运行的所有阶段。

3、任何语言。开发者敏捷性意味着开发者在构建他们的应用的时候可以自由选择使用任何应用特性需要的编程语言、版本和工具。同时,在同一时间运行一个语言的多个版本的能力也为开发者提供了极大的灵活性。Docker让你的团队更加关注于构建应用程序本身,而不是思考如何构建一个可以在Docker中运行的应用。

4、任何操作系统。 绝大多数的组织拥有不止一款操作系统。一些工具在Linux上工作的更好,而另外一些可能在Windows上运行的更优异。应用平台需要考虑和支持这种多样性。否则,只能算是解决了部分问题而已。Docker起初是为Linux社区量身打造的,但Docker和微软公司正着手在Windows Server上实现Docker,以支持数百万现存企业应用以及未来企业应用。

5、任何基础设施。 谈到基础设施,组织想要的是选择、备份和杠杆作用。这是否意味着你需要拥有多个私有数据中心,一个混合云或者多个云提供商呢,其实关键点在于具备将应用负荷在不同环境间迁移而又不出问题的能力。Docker技术架构将基础设施与应用分离,使得应用容器可以在横跨基础设施在任意基础设施上运行。

6、Open API,插件式架构和生态系统。 一个平台不能算作是一个真正的平台,如果它只是一个封闭的孤岛。如果你想首先改良更新你现有的环境,通过实现新技术一般是不可行的。Docker的一个基本指导原则就是一个开放的平台。开放意味着API和插件可以让你利用上你已有的投资并让Docker适应你的环境和过程。开放性可以让生态系统更加活跃,且当你的CaaS增加特定功能时,它可以给你提供更多的灵活性和更多的选择。

虽然CaaS具有许多特性,但上述这些特性却是关键的,因为这种新的定制化应用范式只是为你的技术架构引入了更多异质性。Docker CaaS平台根本上就是为了支持这种多样性而设计的,并且针对任意规模提供相应的控制能力。

五、Docker CaaS

平台组件: Docker CaaS平台由一系列集成软件方案以及一个灵活的部署模型组成,以满足你的业务需求。

img{512x368}

本地数据中心/虚拟私有云(VPC): 对于那些要使用自己网络的组织,Docker Trusted Registry和Docker Universal Control Plan可以被部署在本地数据中心或虚拟私有云中,并且可以连接你已有的基础设施以及系统,比如存储、Active Directory/LDAP以及监控与日志解决方案。映像文件存储在你自己的存储架构中,Trusted Registry提供存储和管理服务能力,并且同时提供基于角色的对映像的基本访问控制。Universal Control Plane提供对Docker环境的可视化管理,包括Swarm集群、Trusted Registry仓库,容器以及多容器应用。

在云中: 对于那些接受使用SaaS方案的组织来说,Docker Hub和Docker Cloud提供了基于Docker上运行和管理的registry和control plane服务。Hub是一个云Registry服务,用于存储和管理映像文件以及用户权限。Docker Cloud供应和管理部署集群,同时也监控和管理已部署应用。使用Docker Cloud连接到你选择的云基础设施或使用你自己的物理节点来部署你的应用吧。

你的Docker CaaS可以设计成集中控制和管理,也可以设计成分布式管理以授权给各自应用团队。这种灵活性使得你可以建立一个最适合你的业务的模型,就像你选择基础设施和内容实现过程那样。CaaS是构建、交付和运行应用理念的一个延伸。

事实上由于CaaS统一了跨环境的本质,加速了许多IT倡议被接纳的过程。每个组织都有其自己采纳的倡议:从容器化,包括对已有应用的改造和迁移,到微服务,再到持续集成、持续交付和devops以及对各类云的接纳、迁移、混合及支持多种云。在每个场景中,Docker CaaS都能带来敏捷性、可移植性和控制,使得组织能接受那些用例。

六、AND的力量

总之,云、应用和数据的变化已经将技术和商业之间的对话,从“你如何帮我削减成本”换成了“你如何加速我的商业”。当你踏上你的旅途时,Docker提供了额外的灵活性帮你选择在哪里存储你的应用内容以及在哪里部署你的控制台。让你的CaaS适配你的业务需求,不管是部署在本地数据中心或虚拟私有云上,还是作为云服务被平滑地消费。无论你的业务是什么,Docker CaaS平台都会提供敏捷性、可移植性和控制力,尽可能又快又好的构建最好的应用,以最优的代价提供峰值性能的服务,并且不会被平台锁定。

利用ZooKeeper服务实现分布式系统的Leader选举

每次与Java组的同事们坐下来谈技术、谈理想、谈人生时,Java组的同事总会向我们投来羡慕的眼光:卧槽!又是自己开发的工具,太NB了。这时C程序 员们的脸上就会洋溢出自豪的笑容,然后内心骂道:谁让我们没有现成的呢。另一个空间里的某些“无C不欢”们或者某些“C Guru”们会骂道:靠,有了也不用,自己写!

有时候,C程序员真的有一种下意识:不情愿使用其他语言开发的工具、框架或服务,且比其他程序员更爱“重新发明轮子”(有利有弊)。也许这是某种 骨子里的自负在搞怪;另外一个极端:今天和我聊天的一个经验丰富的C程序员还在忧虑:如果离职是否有公司会要他:(。

其实这个时代的C程序员一直活得挺纠结^_^。

这个世界,软硬件发展日新月异,越来越多的后端程序用Java等其他语言实现。Java高级选手在这个世界上也甚是吃香,这个你看看各大招聘网站 就知道了。再听听坊间“BAT”三巨头给出的高高在上的offer价格,也可以看出Java程序员是多么的有“钱途”和受欢迎了。当然拿好offer的前提是你的Java底子不薄。

其实无论用什么编程语言,成为牛人后,钱途也都是杠杠的。

没有什么好的开场白,于是有了上面一些“胡言乱语”。我们言归正传。

本文是一篇初级技术博文。讲的是如何使用ZooKeeper C API通过ZooKeeper的服务实现分布式系统的Leader选举。当然这一试验是为了尝试解决我们自己的分布式系统在集中配置数据分发这一环节上的 一个“固疾”。还好我还不那么纠结,也没有重新实现ZooKeeper的冲动,于是我就用了ZooKeeper这一Java实现的成熟的分布式 系统的服务框架。

* 搭建ZooKeeper服务环境

    – 下载官方stable release版本 – ZooKeeper3.4.5。解压后,将$ZooKeeper_INSTALL_PATH/bin加入到PATH变量中(其中ZooKeeper_INSTALL_PATH为解压后ZooKeeper-3.4.5目录的绝对路径)。

    – 试验环境下,最简单的ZooKeeper用法就是使用单机版。
      进入到$ZooKeeper_INSTALL_PATH/conf下,将zoo_sample.cfg改名为zoo.cfg,即可作为单机版ZooKeeper的配置文件。当然你也可以像我一样随意修改修改:

      # The number of milliseconds of each tick
   tickTime=2000
   # The number of ticks that the initial
   # synchronization phase can take
   initLimit=5
   # The number of ticks that can pass between
   # sending a request and getting an acknowledgement
   syncLimit=2

   dataDir=/home/tonybai/proj/myZooKeeper
   # the port at which the clients will connect
   clientPort=2181

       
      如果你要体验多机版ZooKeeper服务,那你还要继续动动手脚,以双机版为例,假设有两个ZooKeeper节点(10.0.0.13和10.0.0.14):

      10.0.0.13上的ZooKeeper节点1的配置文件如下:

     # The number of milliseconds of each tick
   tickTime=2000
   # The number of ticks that the initial
   # synchronization phase can take
   initLimit=5
   # The number of ticks that can pass between
   # sending a request and getting an acknowledgement
   syncLimit=2

   dataDir=/home/tonybai/proj/myZooKeeper
   # the port at which the clients will connect
   clientPort=2181

   server.1=10.0.0.13:2888:3888 
   server.2=10.0.0.14:2888:3888

     10.0.0.14上的ZooKeeper节点2的配置文件如下:

     # The number of milliseconds of each tick
   tickTime=2000
   # The number of ticks that the initial
   # synchronization phase can take
   initLimit=5
   # The number of ticks that can pass between
   # sending a request and getting an acknowledgement
   syncLimit=2

   dataDir=/home/tonybai/proj/myZooKeeper
   # the port at which the clients will connect
   clientPort=2181

   server.1=10.0.0.13:2888:3888
   server.2=10.0.0.14:2888:3888

      别忘了在每个节点的dataDir下分别创建一个myid文件:
      在10.0.0.13节点1上执行:
      
     $> echo 1 > myid

      在10.0.0.14节点2上执行:
     
   $> echo 2 > myid

      启动ZooKeeper执行:
      $> zkServer.sh start

      模拟一个客户端连到ZooKeeper服务上:
      $> zkCli.sh

      成功链接后,你将进入一个命令行交互界面:
       [zk: 10.0.0.13:2181(CONNECTED) 1] help
    ZooKeeper -server host:port cmd args
    connect host:port
    get path [watch]
    ls path [watch]
    set path data [version]
    rmr path
    delquota [-n|-b] path 

        … …

* 选主原理

   ZooKeeper在选主过程中提供的服务就好比一栋名为"/election"小屋,小屋只有一个门,各节点只能通过这个门逐个进入。每个节点进入后, 都会被分配唯一编号(member-n),编号n自小到大递增,节点编号最小的自封为Leader,其他节点只能做跟班的(follower) – 这年头还是小的吃香:原配干不过小三儿,小三儿干不过小四儿,不是么^_^!)。
   每当一个节点离开,ZooKeeper都会通知屋内的所有节点,屋内节点收到通知后再次判断一下自己是否是屋内剩余节点中编号最小的节点,如果是,则自封为Leader,否则为Follower。

   再用稍正式的语言重述一遍:

   各个子节点同时在某个ZooKeeper数据路径/election下建立"ZOO_SEQUENCE|ZOO_EPHEMERAL"节点 – member,且各个节点监视(Watch) /election路径的子路径的变更事件。ZooKeeper的sequence节点特性保证节点创建时会被从小到大加上编号。同时节点的 ephemeral特性保证一旦子节点宕机或异常停掉,其对应的member节点会被ZooKeeper自动删除,而其他节点会收到该变更通知,重新判定 自己是leader还是follower以及谁才是真正的leader。

* 示例代码

关于ZooKeeper的C API的使用资料甚少,但这里就偏偏要用C API举例。

C API的安装方法:进入$ZOOKEEPER_INSTALL_PATH/src/c下面,configure->make->make install即可。

ZooKeeper的C API分为同步与异步两种模式,这里简单起见用的都是同步机制。代码不多,索性全贴出来。在这里能checkout到全部代码。

/* election.c */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include "zookeeper.h"

static int
is_leader(zhandle_t* zkhandle, char *myid);

static void
get_node_name(const char *buf, char *node);

struct watch_func_para_t {
    zhandle_t *zkhandle;
    char node[64];
};

void
election_children_watcher(zhandle_t* zh, int type, int state,
                      const char* path, void* watcherCtx)
{
    int ret = 0;

    struct watch_func_para_t* para= (struct watch_func_para_t*)watcherCtx;

    struct String_vector strings;
    struct Stat stat;

    /* 重新监听 */
    ret = zoo_wget_children2(para->zkhandle, "/election", election_children_watcher,
                             watcherCtx, &strings, &stat);
    if (ret) {
        fprintf(stderr, "child: zoo_wget_children2 error [%d]\n", ret);
        exit(EXIT_FAILURE);
    }

    /* 判断主从 */
    if (is_leader(para->zkhandle, para->node))
        printf("This is [%s], i am a leader\n", para->node);
    else
        printf("This is [%s], i am a follower\n", para->node);

    return;
}

void def_election_watcher(zhandle_t* zh, int type, int state,
        const char* path, void* watcherCtx)
{
    printf("Something happened.\n");
    printf("type: %d\n", type);
    printf("state: %d\n", state);
    printf("path: %s\n", path);
    printf("watcherCtx: %s\n", (char *)watcherCtx);
}

int
main(int argc, const char *argv[])
{

    const char* host = "10.0.0.13:2181";
    zhandle_t* zkhandle;
    int timeout = 5000;
    char buf[512] = {0};
    char node[512] = {0};

    zoo_set_debug_level(ZOO_LOG_LEVEL_WARN);
    zkhandle = zookeeper_init(host, def_election_watcher, timeout,
                              0, "Zookeeper examples: election", 0);
    if (zkhandle == NULL) {
        fprintf(stderr, "Connecting to zookeeper servers error…\n");
        exit(EXIT_FAILURE);
    }

    /* 在/election下创建member节点 */
    int ret = zoo_create(zkhandle,
                        "/election/member",
                        "hello",
                        5,
                        &ZOO_OPEN_ACL_UNSAFE,  /* a completely open ACL */
                        ZOO_SEQUENCE|ZOO_EPHEMERAL,
                        buf,
                        sizeof(buf)-1);
    if (ret) {
        fprintf(stderr, "zoo_create error [%d]\n", ret);
        exit(EXIT_FAILURE);
    }

    get_node_name(buf, node);
    /* 判断当前是否是Leader节点 */
    if (is_leader(zkhandle, node)) {
        printf("This is [%s], i am a leader\n", node);
    } else {
        printf("This is [%s], i am a follower\n", node);
    }

    struct Stat stat;
    struct String_vector strings;
    struct watch_func_para_t para;
    memset(&para, 0, sizeof(para));
    para.zkhandle = zkhandle;
    strcpy(para.node, node);

    /* 监视/election的所有子节点事件 */
    ret = zoo_wget_children2(zkhandle, "/election", election_children_watcher, &para, &strings, &stat);
    if (ret) {
        fprintf(stderr, "zoo_wget_children2 error [%d]\n", ret);
        exit(EXIT_FAILURE);
    }

    /* just wait for experiments*/
    sleep(10000);

    zookeeper_close(zkhandle);
}

static int
is_leader( zhandle_t* zkhandle, char *myid)
{
    int ret = 0;
    int flag = 1;

    struct String_vector strings;
    ret = zoo_get_children(zkhandle, "/election", 0, &strings);
    if (ret) {
        fprintf(stderr, "Error %d for %s\n", ret, "get_children");
        exit(EXIT_FAILURE);
    }

    /* 计数 */
    for (int i = 0;  i < strings.count; i++) {
        if (strcmp(myid, strings.data[i]) > 0) {
            flag = 0;
            break;
        }
    }

    return flag;
}

static void
get_node_name(const char *buf, char *node)
{
    const char *p = buf;
    int i;
    for (i = strlen(buf) – 1; i >= 0; i–) {
        if (*(p + i) == '/') {
            break;
        }
    }

    strcpy(node, p + i + 1);
    return;
}

编译这个代码:
$> gcc -g -std=gnu99 -o election election.c -DTHREADED -I/usr/local/include/zookeeper -lzookeeper_mt -lpthread

验证时,我们在不同窗口启动三次election程序:

窗口1, election启动:

$> election
Something happened.
type: -1
state: 3
path:
watcherCtx: Zookeeper examples: election
This is [member0000000001], i am a leader

窗口2,election启动:

$> election
Something happened.
type: -1
state: 3
path:
watcherCtx: Zookeeper examples: election
This is [member0000000002], i am a follower

此时窗口1中的election也会收到/election的字节点增加事件,并给出响应:

This is [member0000000001], i am a leader

同理当窗口3中的election启动时,窗口1和2中的election都能收到变动通知,并给予响应。

我们现在停掉窗口1中的election,大约5s后,我们在窗口2中看到:

This is [member0000000002], i am a leader

在窗口3中看到:

This is [member0000000003], i am a follower

可以看出窗口2和3中的election程序又做了一次自我选举。结果窗口2中的election由于节点编号最小而被选为Leader。

如发现本站页面被黑,比如:挂载广告、挖矿等恶意代码,请朋友们及时联系我。十分感谢! Go语言第一课 Go语言进阶课 Go语言精进之路1 Go语言精进之路2 Go语言第一课 Go语言编程指南
商务合作请联系bigwhite.cn AT aliyun.com

欢迎使用邮件订阅我的博客

输入邮箱订阅本站,只要有新文章发布,就会第一时间发送邮件通知你哦!

这里是 Tony Bai的个人Blog,欢迎访问、订阅和留言! 订阅Feed请点击上面图片

如果您觉得这里的文章对您有帮助,请扫描上方二维码进行捐赠 ,加油后的Tony Bai将会为您呈现更多精彩的文章,谢谢!

如果您希望通过微信捐赠,请用微信客户端扫描下方赞赏码:

如果您希望通过比特币或以太币捐赠,可以扫描下方二维码:

比特币:

以太币:

如果您喜欢通过微信浏览本站内容,可以扫描下方二维码,订阅本站官方微信订阅号“iamtonybai”;点击二维码,可直达本人官方微博主页^_^:
本站Powered by Digital Ocean VPS。
选择Digital Ocean VPS主机,即可获得10美元现金充值,可 免费使用两个月哟! 著名主机提供商Linode 10$优惠码:linode10,在 这里注册即可免费获 得。阿里云推荐码: 1WFZ0V立享9折!


View Tony Bai's profile on LinkedIn
DigitalOcean Referral Badge

文章

评论

  • 正在加载...

分类

标签

归档



View My Stats