用例——大小重要吗?大规模基础设施中的CI/CD

文摘:

Loreli Cadapan和Rohit Kumar/ Oracle, 2016年5月:度量驱动的复杂编排:为了将多个系统(Jenkins、Artifactory、分布式测试环境)上的异步处理编排成一个整体的持续交付管道,我们开发了一种基于clojure的领域特定语言(DSL),它为Oracle产品团队提供了高级宏来表达和绘制复杂的、高度可配置的CI/CD工作流。除了产品持续交付管道的样例用例外,我们还将介绍与此基于事件的编排服务器的使用相关的统计信息。
大规模的Docker注册表:
在Oracle,许多产品团队使用docker作为他们云持续开发的一部分。每个产品团队能够使用自己的内部docker注册表基于Artifactory的docker支持,使团队能够在不同的注册表中管理他们的项目,对他们的docker映像进行更好的访问控制,并在整个组织中共享映像。

讨论转录:

[洛蕾丽]所以,你可能已经读过标题了。所以尽管我们的演讲叫这个名字,我可以向你们保证唐纳德·特朗普不会出现在这里。对不起,吉姆。抱歉让你失望了。

我们要讨论的是如何在Oracle这样的大型组织中扩展持续集成和持续交付。我的名字是Loreli和我的搭档是Rohit Kumar。让我给你们简单介绍一下我们是谁,我们为之工作的团队。我们在Oracle的中间件中心开发工程团队。我们团队的目标是提高产品质量,提高开发人员的生产力,让CI或CD更快,提高我们管道的速度,并使产品团队能够自主地完成他们的产品构建,并能够提高他们的产品生产力。

这是我们的安全港声明。我让你们读一会儿。如果你能阅读,希望。好吧。

好吧。成长的痛苦。所以在像我们这样的大公司,我们必须经历各种成长的痛苦。但最大的问题,我可以肯定地说,最大的两个问题是依赖管理和我们如何管理二进制文件的生命周期。我们如何使用二进制文件。另一个是持续集成,跨产品团队和跨分布式系统的持续交付。让我给大家介绍一下Oracle的背景以及我们想要如何改进。

所以从本质上讲,我们想改善我们消费依赖关系的方式。在过去,开发团队必须通过源代码所在的SCM来消费他们的依赖项。所以我们需要解耦。其他的都能进行测试。因此,为了能够进行测试,我们不仅需要支持我们的持续集成系统,还需要支持我们的测试场、二进制存储库等等。

好吧。第一个成长的痛苦是我们的依赖管理。同样,我们有同样的工具,我们的源代码控制,以及我们的二进制文件。所以它是紧密耦合的我们想要解耦。ADE是我们当时使用的SCM。现在我们希望能够摆脱这种情况,为产品团队提供灵活性,使他们能够以不同的方式消费。

另一个是过程粒度消费。我们意识到产品团队,因为他们从标签或ADE标签中消费,他们基本上只是说,好吧,我要依赖这个完整的目录结构。所以没有一种方法可以在更小的粒度内定义依赖关系。

另一个是循环依赖。产品团队相互依赖,非常不和谐,很难管理。

最终我们意识到,我们需要改变。我们需要继续前进。我们需要从仅仅拥有一个紧密耦合的二进制存储库开始,因此我们决定使用Artifactory。Artifactory是我们在评估了其他几个供应商(如Nexus)后决定使用的。我们选择Artifactory的主要原因是它提供了更好的服务,更好的支持,以及更好的产品。

现在,我们在6个不同的数据中心有6个Artifactory实例。它们都是HA实例。最主要的是四个节点,最终变成五个节点。因此,现在在中间件组织中,它们都通过Artifactory进行消费。没有更多的消费通过他们的标签等等,这实际上增加了我们管道的速度。

我们去年就讲过这个了。所以它可能不再像今年那样令人印象深刻了,但我想给你们一个续集或延续我们所处的位置。现在我们有80tb。所以我们使用了80tb的存储空间。4710万件文物。我们每天大约有39.1个请求。我们服务39.1个请求。其中大多数实际上是下载请求或拉取请求。大约有150万是出版的。实际上,上周我们做了一个快速分析,看看我们每天有多少tb的数据流。 And we realize we’re actually producing about 85 terabytes per day on that. Again, six global data centers and our nodes are typically about 256 gig in memory size.

所以加速。在2013年初,我们在Artifactory的存储方面是零起步的。现在是80tb。所以你可以看到,我不知道你是否能分辨出蓝色和黑色标记的区别,所以在2015年左右,2015年底,我们意识到,我们需要一个更好的模型来管理我们的存储。我们如何管理存储的大小?因此我们决定我们需要做一些持续的删除,持续的清理我们的存储库。所以在这个时候,我们稳定了存储空间的使用然后在黑色标记处我们决定,我们需要一些方法来控制配额。对不同的产品团队进行配额管理。我们意识到一些产品团队在使用方面比其他团队更霸道。因此,我们需要确定这些产品团队,并确保我们,你知道,我们正在管理存储等等。

好吧。第二种成长的痛苦。CI / CD。那么我们如何扩展我们的持续集成和持续交付?正如我之前提到的,我们不仅支持持续集成系统,而且还支持其他分布式系统,比如我们的测试农场、Jira,以及我们需要能够在管道中集成的各种其他系统。因此我们意识到,好吧,我们不能只做一些,我们支持的Jenkins工作,然后把它们连接到一个管道中。我们需要更好的东西。我们意识到我们需要引入一个新的系统调用,它本质上是一个基于事件的系统。

同样,管道跨越了几个开发团队。这些团队的规模各不相同。现在我们需要支持6000名开发者。所有的构建都是并行的,都发布到Artifactory。我们意识到的另一件事是一些开发工具实际上负担过重。我们不能只使用一个Jenkins实例,我们需要一个多主实例。

另一个是可整除性。我们如何获得管道的可见性?谁在推广什么?产品团队多久做一次促销?什么是产品团队——实际上没有推广的产品团队等等?

所以我们想到的是,让我给大家介绍一下我们是如何开始的。我们从一个大师开始。实际上是当时的哈德逊大师。詹金斯。很明显,这个master是内存绑定和IO绑定的。但我们意识到,好吧,我们需要能够支持多个团队在开始的时候,当我们加入团队的时候,大约有两三个产品团队实际上加入了团队,他们很好。他们对詹金斯的一个例子很满意。但是不久之后,我们意识到,我们不能在一个Jenkins实例上支持6000个开发人员。所以我们想出了一个多大师的方法。

所以我们决定建立一个组织,每个组织都有自己的负责人,哈德森或詹金斯负责人。还有指定数量的奴隶。每个大师,或者每个组织,都能够控制他们的插件,控制他们想要的插件的版本。并控制需要安装在奴隶上的各种软件。

好吧。最后我们意识到一些组织,或者一些主人,有一些非常空闲的奴隶。我们需要想出一种方法,能够更有效地利用我们的资源,更有效率。2022世界杯阿根廷预选赛赛程所以我们决定将Mesos整合到这款游戏中。这也使我们能够更有效地利用资源,也能够向其他系统提供我们拥有的一些资源,比如我们的测试农场。2022世界杯阿根廷预选赛赛程同时,基于这种整合,我们能够得到一些分析和报告,关于我们一次真正需要多少资源。2022世界杯阿根廷预选赛赛程哪些组织比其他组织需要更多的资源?2022世界杯阿根廷预选赛赛程

然后我们想到,我们有多主,我们有各种Jenkins,但是我们如何把它集成到管道中。进入推广渠道。我们如何测试,我们如何测试我们的产品构建,比如说,我们的测试场或者我们称之为D到E拓扑。我们如何整合我们的文档-文档翻译,wptg?我们如何整合我们的补丁系统?我们如何集成Artifactory?我们如何整合我们的可视化?因此,我们想到了卡森。

所以Carson本质上是一个基于事件的编排。我们这里有一个人是这一切的始作俑者。那是他的孩子,我们已经把他的孩子照顾得差不多了。自从他离开甲骨文之后。但是卡森已经进化了。此后,卡森扩大了规模。我们也有了更多的产品团队。所以不仅仅是内部团队,我们还有云团队,我们还有文档,产品团队。

我将让罗希特从本质上讲一下这个架构图但本质上卡森所提供的好处卡森所提供的好处是能够在我们的组织中整合这些分布式系统。

这是我们最初拥有的一个推广管道的例子。一开始是一个开发人员签入代码变更。将其检查到他们正在使用的SCM中,无论是Perforce, ADE还是Git,然后从那里能够触发构建,对吧。在像Jenkins或Hudson这样的CI系统上。然后可以发布到Artifactory上。一旦他们发布到Artifactory,这个管道,本质上,将测试特定的产品与最新推广的产品。

现在这种情况每天都会发生好几次。以前发生的频率要低得多。然后我们还有第二条管道,就在这里。这被称为二级管道。这基本上就是做我们的集成测试。我们如何将所有的产品团队——所有的产品整合到一个管道中?hth华体会最新官方网站

在此基础上,我们意识到卡森可以做的一些事情是我们有460个编曲。我刚才给你们看的管道,只是一个管弦乐。我们有460种不同的编曲是产品团队通过卡森创建的。每天,我们有大约18000个编组执行,至于我们支持多少个Hudson和Jenkins大师,我们现在大约有238个。515 -大约515个vm用于我们的从属代理。

就事件而言,卡森是如何扩大规模的?卡森每天处理大约81000个事件。其中大约一半或略多于一半实际上来自我们的CI服务器。我们的哈德森和詹金斯。还有一些是从我们的试验农场来的。我们在那里做测试本质上,当我们在管道中做促销和测试时,我们需要得到关于产品是否提前飞行或是否成功的事件。这些都是基于事件。

好吧。所以我让罗希特继续卡森。

[罗希特]谢谢你,罗瑞丽。

[洛蕾丽]谢谢。

罗希特好。回到。正确的。所以,Carson的出现是因为我们需要一个系统可以协调,很多应用程序,多个CI服务器实例。产品,比如,产品团队A可以有一个大师,但需要找一份工作,另一个詹金斯大师。这就是卡森出现的第一个原因。所以。卡森的特点,你知道,它提供了,我们称之为编排服务器。

它基本上是用于协调跨多个分布式系统的工作流。这到底是什么意思呢?所以卡森是事件驱动型的。我们意识到的一件事是,如果你在一个主服务器上有管道,它需要调用作业,在另一个系统或另一个主服务器上,而不是像拉取状态,或依赖所有这些,如果你完全依赖事件流,不管它们来自其他Jenkins主服务器,还是来自其他分布式应用程序。它实际上使编排更有效,更可靠。

举个例子。scm,更大的scm。例如,你有一个Git提交,有人将一个更改推送到master。正确的。所以任何使用Git的SCM,无论是Oracle自己的SCM,诸如此类的东西,你只需使用json编码的数据,它封装了一些状态变化,一些,你知道,事务信息。

显然是Hudson和Jenkins CI服务器实例上的作业。当构建成功时,当构建失败时,当构建开始时,诸如此类的事件。类似的测试,测试,完成测试,开始等等。你可以想象来自Jira系统的事件。Jira的应用程序就像有人缓解了一个问题,一个问题产生了。你得到不同类型的故事,部署和更改请求,它们被接受,你得到一个事件,你可以,你可以把你的管道向前推进。对吧?

所以我们基本上是通过使用中间的消息传递层来获取事件。因此,我们目前使用的是[…]B代理,例如[…]。同时,我们也提供了一个webhook子系统,在这个子系统中,你可以通过一个简单的HTTP发布和通知,然后通知认证服务器。所以这允许我们做的是-它的编写方式是你可以随时插入新的事件流。或者新的工作系统。所以很多状态都是可插拔的,当我谈到架构时,它会变得更清晰一些。

这就是它的结构。我知道天气有点朦胧。你看不出来。所有的工作系统都在左边事件源在左边。所以你可以有多个Hudson master,多个Jenkins master。任何类型的CI服务器。所以我们所需要的,就像我们对所有这些工作系统或事件源的要求一样,就是我们需要从它们那里得到事件。我们有Jenkins和Hudson的插件,当我们提供一个新的master时,它们就会被部署,插件会生成事件。因此,构建完成,构建开始,我们能够将事件发布到消息代理。

类似于我们在Oracle内部的其他一些东西,比如文档和翻译应用程序以及测试场。当然,就像Jira和Artifactory提供的,尤其是Jira,提供了一个webhook插件,你可以在其中定义事件——你可以定义,一个webhook什么时候会被激活。它会寄什么样的海报,诸如此类。对吧?scm也是一样。

左边是事件流和作业系统,中间是消息传递层,也就是webhook和消息传递层,也就是broker。消息进入那里,它们进入身份验证引擎,然后简单地驱动管道。所以一个工作结束了,一个工作开始了。因为我们有很多事件,我们可以在这些事件之间进行协调。对吧?

Carson的一些元素,我的意思是,很明显它有一个REST API,有一个UI,它由事务数据库支持。正如我提到的,事件流和作业系统都是可插拔的。所以你总是可以,如果你需要定义一个新的,如果一个新的,假设你有一个新的CI服务器,你知道,你想要开始发送事件,创建一个管道,你可以,你可以插入它。这是另一件事,我们只需要一个REST API。如果你是一个工作系统,我们需要一个REST API和职业语言。这意味着我们需要能够使用REST API来调用,一个作业,一个启动器构建在你的应用程序上,在你的CI服务器上。对吧?另外,我们提供的语言是DSL。我们提供了一个DSL,您可以使用它来编写跨这些作业系统的编排。还有一些相关的服务,使用Carson来提供初始化,提供分析,并把数据馈送到认证服务器。 Right?

这是架构图的大视图,现在我想多花点时间,看看这和Jenkins 2.0管道插件有什么不同。对吧?其中管道是CS服务器中的一个特性。

其中一件事是,我的意思是,我一直在开车。事件流可以来自任何地方,如果你有一个管道,你知道,今天,它是Jenkins,但是明天你想用CD Go。或者你想去其他CI服务器。如果您希望能够跨这些实例进行编排,Carson为您提供了该功能。它为您提供了这种能力。正确的。作业系统就像事件生成器,执行实际任务和步骤它们通常就像你在那里做的同步操作。Carson的编排服务器主要是异步操作。它总是被分派的动作并对返回的事件做出反应。它没有自己进行处理。 So it’s just lightweight event handling and dispatch, right. So any kind of workflow as well. So any kind of workflow that you want to implement. You use the authentication server for that.

所以,我们允许用户定义他们的管道的方式,因为现在有一个新的应用程序,你知道,跨越所有这些不同的工作系统。以及事件流。对吧?所以,卡森从一开始就写完了。因为它是在结束时写的,我的意思是它在结束时写的原因之一是我们不知道什么样的,你知道,事件流将会出现,什么样的新工作系统将必须被纳入。它使我们能够创建一个不断发展的DSL。对吧?

因此,使用Closure DSL定义编排,并由编排引擎编译。DSL的基本元素就像一个处理程序,它基本上由三部分组成。这三样东西是,你正在响应的通道,作业,或事件流。它就像一个你根据传入的事件流定义的通道。哈德森,詹金斯,农场之类的,对吧?Jira。这是第一个元素。

第二个是,你要在那个事件流上应用什么样的过滤器。一个任务是否成功,如果它是一个任务事件。失败的地方,诸如此类。这些行为是指,你所做的下游活动。如果你把动作想象成,管道中的下游节点或作业,从当前节点连接起来,对吧。DSL通常跨越多个主机,多个Jenkins主机,多个作业系统和事件源。它允许你做一些事情,比如,如果你有一个中心任务,假设它扫描你的代码,对吧?多个管道可以调用它。作业本身不需要改变任何东西,您知道,它可以是多个管道的一部分,而编排将负责将作业的特定构建与编排(调用它的编排实例)进行关联。对吧?

稍微讲一下DSL是什么样子的。它显然是Lisp,它看起来是这样的。所以,好的。我是《终结者》的超级粉丝,所以叫T800。

这是最简单的,一个简单的编曲,对吧?这里有一个deforg符号这里有一个def start处理程序是启动管道实例的处理程序?管道的一次运行。回到处理器的三个元素,它是通道,谓词和动作块。如果你想展开这个DSL在做什么,它会说,来自DCS通道的事件,这是开发者代码服务,Git之类的东西。在这个事件通道上,任何进入检查的事件,Git提交或Git推送中的组织都是myorg,项目就是这个项目。对吧?很明显,所有这些滤波器都是一起的所以这是一种,一种操作,对吧?你可以,我待会儿会讲到,你可以定义你自己的自定义谓词,对吧?我的意思是编排就像轻微的沙盒但是你可以自由定义,你知道,定义非常复杂的过滤器,对吧。 Like if you want to do, like check that this is Monday if you committed, you know, into the readme or do anything like that.

然后我们来到第三部分。这是then块,这是动作块。这是你分派动作的地方。正确的。您采取进一步的行动,使管道向前移动。就像我说的,我们调度工作的所有作业系统的接口通常只是一个REST API调用。显然Hudson和Jenkins提供了一个REST API。这是有问题的,但我们仍然能够解决这些问题,你知道,从你想要的工作开始。

例如,这个特定的start job,它所做的是说,对于我的项目构建,这是一个作业,在Jenkins或Hudson上叫做myorg,开始一个新的构建,取这些参数,有一个参数名,有一个参数值,在这个协调的上下关系中,叫它build job。正确的。

第二个是,我只是在给你展示一个复杂的DSL元素它对于你可能需要的编排能力来说是非常独特的。这个是说,如果在这个特定实例的编排配置中,有一个动态指定的作业列表。如果我需要触发5个不同的作业,它们每次都可以延迟,对于同一条流水线的每次运行。但我还是想用同样的方式来回应。所以你可以这样做,你把你的,你现在的,你开始的工作包裹成一个整体块,它会调度,你知道,每个工作,每个工作的构建。然后你就能对整个作业集或一组作业一起完成做出反应。正确的。

这是一些例子。我们从defstart开始。DSL还有一些其他元素有defstart, deftransition等等。但基本上他们都在做同样的事情。你知道,你有一个通道或者你有一个先前在编排中被调用的作业当它完成或者当一个事件进入一个通道时,你对它做出反应,对吧?

例如,这个是,一个Hudson任务完成,开始一个任务名称的构建[…]完成后,开始这两个工作,但将其称为一个组。对吧?在你的初始化中,你实际上看到它们,你知道,这两个工作是相关的,我想在这两个工作中引用它们[…]。正确吗?而转移,它所需要的只是之前开始的构建,你可以,你知道,依靠完成。编排引擎就像是抽象出所有的事件处理,它能够关联你之前已经分派的任务,无论它是否完成。因为它从所有这些作业系统和事件流中获得了大量的事件。对吧?

就像我在之前的幻灯片中展示的,有了start jobs宏,身份验证引擎的基本功能之一就是它允许你做动态。所以。所以你不受平面图静态形状的限制,你可以定义你的管道,然后说管道中的这个特定块将被动态渲染。对吧?我不知道这里会执行什么样的任务,我会让开发者决定,让一些配置决定,诸如此类。对吧?

给大家举个例子。例如,一个翻译请求进来。你有一个单独的哈德逊任务来完成这个操作。对吧?它做出了一些改变,它建立了一些东西。有时候你可能会得到一个请求去内置,比如30种不同的语言,有时候你可能会得到一个请求去内置,比如两种语言,诸如此类。对吧?但在你的管道中,在你的初始化中,它只是,我得到的所有请求,我都在构建它们,每次构建都在发生,针对我得到的每个不同的条目,但我能把它初始化为一组作业。对吧?它给了你并行性,但它也给了你一个抽象层用于报告初始化。 That is pretty useful for any kind of — any kind of workflow, right. Not just —

好的,一些特征的元素。就像我之前提到的,为什么我们选择关闭。有一个LISP,有函数,协议,多方法等等。正确的。例如,您在这里看到的谓词过滤器称为success。它不关心,你不需要在这里详细说明你申请的是什么样的工作。对吧?Closure的multimethods之类的东西允许你去推断你正在处理的对象的类型。类似的协议,我认为它们是非常一般化的接口。所以我们所有的工作系统都是协议。 Our messaging layers are also implemented as a protocol. So if tomorrow we want to add, let’s say, another broker like Kafka or things like that, we would be able to just plug it in and define the protocol for Kafka, right.

因为DSL只是一个LISP,对吧?代码,我的意思是,代码看起来就像语言。代码看起来像数据。这些是s表达式,你可以,把它像树一样解析这就是我们所做的,我们可以从你写的DSL中构造节点并且可以初始化静态的,你知道的,你的管道运行时的样子。以及基于管道的运行,我们将能够初始化您的编排的不同实例,对吧?

简单地说一下它建立在什么样的闭包库之上。正确的。所以我们在任何地方都完全使用异步处理。这里我们使用的库是code。async。它很轻。你可以很容易地进行异步编程,代码很紧凑。我们已经,我的意思是,我们已经让它为我们工作得很好了。关于每个编排的事情是,因为每个编排就像一个模块,一个闭包模块,你能够,你有完整的Java中断,访问。对吧?显然,你不能做系统斜杠退出之类的事情,但你仍然可以在你的编排中定义你的函数,所以你不是在计划一个整体,你是在创建一个完整的插件。 What you’re able to define custom matching that you want, or custom predicates that you want, to be able to, like, make really complex decisions in your workflow. Right?

所以。DSL还提供了一些非常独特的功能。我想我已经讲过动态工作执行之类的了。所以使用DSL,我们可以像支持的东西一样,你知道,重复,直到成功。或者多次重新提交,直到成功。我们有可插拔的节流协议,您可以为您的编排定义何时节流。所以什么时候节流,什么时候,你知道,当一个事件发生的时候,当你从节流状态中释放一些东西的时候,你该怎么做。它是如何在DSL中工作的,例如,如果我回到这里。

你在DSL中要做的就是,它不会显示这个。在DSL中你所要做的就是把你的动作包装到这些块中。对吧?所以它们就像钩子,改变你的行为。所以如果你想,如果你想在业务流程中限制业务流程它只需要定义一个限制的实现。显然,您可以使用协议的预定义元素和DSL预定义实现。

所以你可以做这样的事情,比如只运行最多5个实例-管道的实例。在那之后,对所有的请求进行排队,然后当你发布时,只做最新的运行,而不是,如果一个产品正在构建,队列中有请求,只取最新的那个。然后运行最新的一个,跳过其他的。

重申一下,我们能够组合所有这些DSL元素,并在运行时根据参数或从另一个系统查询的配置修改执行。所以从这个意义上说,是的,它是非常动态的。即使你知道管道应该是什么样的。真正的跑步可以,你知道,走到奇怪的分支。

因为我们有自己的数据库做备份,所以所有的编排,你知道,就是数据和[…]。我们有一个REST API来提供这一切,这个REST API就像初始化的基础,也是我们报告和分析的基础。对吧?

基本上优化引擎,除了设置,所有这些事件处理器,对吧。在业务流程中定义的任何处理程序。它也在记录事件发生的过程。每一次转换都发生了什么。它知道什么时候作业开始,什么时候作业结束,什么事件使转换发生,诸如此类。这允许我们提供管道的json表示。正确的。

让你们看看DSL,你可以在DSL分析上构建的图表不是很清楚。这张图就是DSL的表示。我们只是通过编译时解析来做那个,DSL。看看DSL的元素,处理程序,处理程序之间的关系。正确的。这样我们就可以很明显地构建,像,管道的基本静态图形块。比如粉色的这些是会动态渲染的方块。正确的。所以它们可能由500或500个工作组成。是的,我们有基于此的报道。

所以我们在展望未来。这主要是在处理中,比如我们得到的事件的预处理,这样我们就能更好地,你知道,更好地划分事件流。编排。因为我们依赖于分布式系统,所以要确保在出现故障时我们不会受到影响。是否存在单点故障,诸如此类。正确的。我们将探索如何,你知道,跟踪我们的部署-部署管道,对吧。持续部署。并提供更多的虚拟可视化和分析。

我让罗瑞丽说完。只是最后的想法。

谢谢你,罗希特。所以这只是一个结论。

我认为,正如你所看到的,我们从根本上设计卡森的方式,使我们能够与新技术相结合,使我们能够跟上我们今天拥有的非常流动和不断发展的技术。

它是基于事件的,基本上也是我们CI/CD模型编排的支柱。不仅如此,我们还能够通过我们面临的其他挑战来利用卡森。其中之一就是配额管理。我们如何管理Artifactory?对吧?所以我们通过卡森实施了配额管理。我们还通过Carson实现了汽车回购供应。这是在任务控制中心成立之前。所以我们实际上已经有了——我们有了ref api来支持向Artifactory提供新的存储库。

能够支持新的组织能够创建新的组织,新的哈德逊,新的詹金斯,供应这些,供应奴隶,并通过卡森分配奴隶。因此,从本质上讲,它是我们CI/CD模型的支柱,这就是我们能够扩展的方式。

至于大小真的重要吗?我们把它留给你来决定。非常感谢您的宝贵时间。

试试免费的CSS整洁它可以让你为你的网站美化样式表。

要么释放,要么死亡