万字剖析架构权衡之道!

共 14690字,需浏览 30分钟

 ·

2024-06-03 22:22


点击下方“JavaEdge”,选择“设为星标

第一时间关注技术干货!

免责声明~

任何文章不要过度深思!

万事万物都经不起审视,因为世上没有同样的成长环境,也没有同样的认知水平,更「没有适用于所有人的解决方案」

不要急着评判文章列出的观点,只需代入其中,适度审视一番自己即可,能「跳脱出来从外人的角度看看现在的自己处在什么样的阶段」才不为俗人

怎么想、怎么做,全在乎自己「不断实践中寻找适合自己的大道」

00-软件架构权衡-我们为什么以及如何进行权衡?

对于“软件架构”这个词有很多定义和含义。而且,“软件开发”、“软件设计”和“软件架构”这三个概念之间存在相当大的重叠,它们在许多方面相互交融。

从核心上看,可以将软件架构视为在构建应用程序时,对不同选择进行权衡的学科。

1 为什么需要权衡以及我们为什么在意?

我们在构建软件时必须进行权衡的原因,与其他学科中的权衡原因相同。计算系统是复杂的,复杂性越高,实现特定系统的全部目标的方式就越微妙。这些目标:

  • 既可以是功能性的(例如提供用户自助服务的能力、处理特定事件、接受某些输入并产生输出)
  • 也可以是非功能性的(例如每秒处理数百万个请求、实现零信任安全、提供100毫秒以下的响应时间)

如金融投资中,人们普遍理解风险与收益之间的权衡。投资越风险,潜在的财务回报越大。投资越安全,预期收益就越小。

同样的规则也适用于计算,特别是在设计分布式应用程序时。许多组织遇到的问题不是他们必须做出某些让步或权衡,而是大多数组织要么对自己所做的权衡不了解,要么缺乏系统的、明确的和有效的方法来进行这些权衡。

本“架构权衡”系列的目的是阐明在软件架构的不同原则之间进行权衡时的决策过程以及此类决策的具体技术影响。

2 我们在权衡什么?

如上所述,大多数与系统和应用程序架构相关的决策都涉及某种程度的权衡。

既然我们已经知道为什么需要讨论、权衡和有意识地思考架构权衡,我们可以谈谈我们实际在系统和应用程序中进行权衡的方面。

这些架构权衡有时分为两大类:

  • 系统的基本架构特性有关(如可扩展性与简洁性
  • 与具体技术、机制和架构风格有关(如同步通信异步通信Kafka消息总线等)。前者更为广泛的权衡类别也决定了更具体的权衡

本文重点讨论第一类架构权衡——基础架构特性。

因此,当我们谈论架构权衡时,我们实际上是在讨论我们希望支持哪些架构特性并将其纳入我们的主要目标。另一方面,我们也在识别那些我们有意识地决定不太关注或完全放弃的架构特性,以便更加重视那些我们认为更重要的特性。

下面是一些架构特性和一些常见的权衡场景。

3 架构特性

当谈论系统或应用程序架构的固有或基础特性时,我们实际上是指一组关键属性,这些属性定义了特定的系统或应用程序。以下是这些特性的一小部分示例。这绝不是一个详尽的列表,还有许多其他的架构特性。

  • 可扩展性
  • 可观测性
  • 可审计性
  • 弹性
  • 响应性
  • 可测试性
  • 互操作性
  • 可维护性
  • 可支持性

这些特性有时被称为“系统属性”、“架构属性”或干脆称为“后缀-ility”属性。

这些系统特性或属性乍看之下似乎是独立的,但实际上,它们中的许多是相互交织的,可能有直接或反向关系。

4 互操作性 vs 可扩展性、弹性和响应性

让我们以互操作性为例。为了使系统具备互操作性,它需要能够轻松地与其他系统进行交互和通信。这通常意味着所有这些系统都需要使用通用协议。它们需要使用共同商定的标准,并且这些标准还需要设计得易于将来的系统也能相对轻松地“插入”到这种通信中。

然而,如果一个系统优先考虑互操作性,这很可能会影响系统的可扩展性。

举个具体的例子,假设现有的应用程序使用REST协议(依赖HTTP)进行通信。假设我们要引入一个新系统。为了使这个新应用程序与现有的应用程序互操作,我们决定该应用程序的所有进出通信都通过REST/HTTP进行。这看起来很合理。

然而,如果将新系统的通信限制在依赖HTTP上,可能会限制其响应能力和可扩展性,尤其是在需要处理大量请求的情况下。假设这个新系统需要每秒处理数百万个请求,并且是异步的(即调用者无需等待应用程序确认其已处理请求)。这种情况下,由于需要异步通信且Kafka协议(理论上)比HTTP开销更小,这种场景可能更适合使用事件驱动技术如Kafka。

换句话说,在这种情况下,互操作性与响应性以及可扩展性呈反比关系。

5  简单性、易上手性和可支持性 vs 响应性

另一个可能不太技术性、更具组织性的权衡发生在决定以易于支持作为主要架构驱动因素时。这可能意味着使用技术团队熟悉的技术。这也可能意味着使用行业内广泛使用和已知的技术和范式,以便新团队成员可以更快更高效地上手。

将可支持性作为首要任务听起来是显而易见的,因为谁不想要一个易于理解、支持并且易于向新开发人员介绍的系统呢?然而,仍然存在成本和权衡。

基于技术或范式的选择是否为大量专业人员所熟知,可能会阻碍架构的许多其他特性。可扩展性、响应性、弹性、可用性、安全性以及系统的许多其他方面很可能会被放在第二位。

例如,考虑需要设计一个处理和存储交易性和强关系数据的金融应用程序。假设实施该应用程序的团队熟悉NoSQL数据存储如MongoDB。虽然MongoDB是一个适合松散关联的文档型数据的优秀选择,但它不适合具有严格和复杂关系的数据。

对于不同实体之间具有复杂关系且需要临时复杂查询来检索这些数据的数据,通常不太适合像MongoDB这样的存储。这类数据通常更适合“传统”的关系数据库如Postgres或MySQL。

如果我们将可支持性作为这个应用程序的主要架构驱动因素,最终很可能会导致应用程序失败,并引发一系列挑战,最终导致应用程序完全无法扩展,数据库成为瓶颈(这是常见的问题)。

6 可维护性 vs 弹性和容错性

一般而言,系统越简单、移动部件越少,越容易维护。使用的技术和部署环境越少,系统运行和维护的速度和难度越低。

例如,在托管云运行服务中运行单实例应用程序要比分布在不同节点、不同集群、云区域和地区的分布式集群应用程序更容易维护。一些组织甚至通过跨多个云环境部署其系统来确保业务连续性、容错性和灾难恢复(迅速流行但有些争议的多云模式)。

由于每个云(Azure、AWS、GCP、IBM、Oracle等)提供商都有一套独特的功能、部署模型和机制,在多个云上维护一组应用程序成为一个显著挑战(通常是无法维持的)。

7 找到平衡点

上述的三个架构权衡示例可能更偏向极端情况。然而,它们代表了许多团队和组织在规划和选择正确的架构权衡路径时所面临的一些非常现实的挑战。

好消息是,您不必在二者之间做出选择。软件架构权衡以及软件开发的现实要更加复杂,实际上是一个选择的渐变。在这里,您可以选择在一定程度上实现可扩展性,同时在一定程度上实现简单性和互操作性。

关键在于如何找到不同架构特性之间的平衡,以及如何做出知情的、有意识的选择。

8 有意识地了解架构权衡

如我们在开头所说,许多,甚至可以说大多数组织,都是无意识地做出权衡。这往往导致这些组织做出错误的权衡,给其业务和底线带来长期且不利的影响。

依赖数字系统的企业必须有一个适当的计划和流程来做出软件架构和技术决策以及权衡。在没有建立正确的架构权衡意识的情况下,这些组织承担着不合理的风险,其影响和可能显著减缓组织进展的概率很大,在最坏的情况下甚至可能造成无法修复的损害。

接下来的部分将讨论如何进行架构权衡的推理和规划,以及一些具体和常见的情况。

01-软件架构权衡-无意识决策的问题

0 前言

错误的技术决策常常会回过头来困扰我们,原因并不在于决策本身是错误的,而是因为做出该决策的人并未充分理解其全部影响,或更常见的,决策者甚至不知道自己在做决策。这是一种无意识的决策。换句话说,问题核心在于缺乏意识或缺乏决策的谨慎。稍后详细讨论这问题。

先澄清“技术决策”含义。这些决策涉及编码/实现、软件设计、架构、供应商选择和与第三方集成,还有非技术性的决策——即产品和业务决策,同样重要。本文以及整个架构权衡系列重点关注技术方面及其与业务的交集。

先统一术语:

  • MVP(最小可行产品)
  • POC(概念验证)
  • Monolith(单体架构):独立应用程序,执行全部或大部分功能
  • Serverless(无服务器):应用程序运行时和部署主要由云管理
  • Microservices(微服务):细粒度、解耦、独立部署的应用程序,每个应用程序负责特定功能

虽然本文主要提到初创企业,但大多数内容适用于任何技术团队或组织。

1 这是什么?

如何增强对技术决策的信心是一个很少被讨论但绝对关键的话题,它对建立一个能够随着组织扩展和增长而提供服务的健康技术和架构战略至关重要。

看典型初创企业的生命周期,这个企业经受住市场挑战,并踏上增长道路。有许多流行的模型描述了这一生命周期的不同阶段,每个模型都有其独特的视角。

下面的心理模型帮助直观理解这种生命周期,重点是决策通常在哪些阶段做出。初创企业的生命周期:

上述显示,我们在MVP阶段开始做出大部分长期的技术决策。

注意,这是开始做出这类决策的阶段。决策过程永远不会停止。MVP阶段(及之后)的决策与POC阶段(及之前)的决策的区别在于:POC阶段的决策通常是临时的。它们只是为尽快推出某种产品的初步雏形。通常,这些产品甚至不是完全功能性的,而更多是展示最终结果的视觉表示。

MVP阶段的技术决策必须开始关注。这就是技术决策意识发挥作用的地方。该阶段的技术决策通常产生长期影响,因为这些决策将成为后续决策的基础,并将影响组织发展方向(至少在技术方面)。

2 错误的决策总是问题的根源吗?

记住,我在开头提到的问题不一定是决策本身的问题,而是缺乏意识和决策的谨慎吗?尽管这可能看起来违反直觉,但错误决策常常可以最小业务影响来解决。若能及时、流畅识别并解决问题,并有一个明确流程来调整决策,其影响不会像原本那么剧烈。

这只有在决策是以有意识的方式做出,并意识到决策正在被做出及其方式时才会发生。

3 示例

假设一个初创企业处早期MVP阶段。团队正开发SaaS,该产品聚合某种数据并将其暴露给外部。

因此,团队须决定如何部署SaaS。基础设施咋样的?应用程序咋构建?有许多方式构建应用程序和部署产品,选择很多。

团队面临选择(假设使用AWS作为云提供商):

3.1 单体架构

将服务结构化为单体应用程序。将所有功能都放在一起。部署在托管的容器编排服务中:

  • 一个数据库
  • 一个应用程序

3.2 微服务

从一开始就将服务设计为由微服务组成。有多个服务和多个数据库。所有内容都部署在容器编排服务中。

3.3 无服务器/函数即服务

目前非常流行的方法。可用AWS Lambda运行应用程序,使用DynamoDB作为数据库。AWS负责运行所有这些的重任。

4 指导方针

团队应选哪种方案?选择的影响是啥?影响多大?应投入多少精力分析每种选择?这些都是隐藏在行间的问题。此外,在每个选择中还有更多细节选择!

现在正是关键时刻,团队需做出一个有意识的有理由的选择。注意,这不一定是正确选择,因为可能有多个正确选择,并且“正确”可能随未来更多背景的变化而改变。

通常,团队会隐含、无意识或在不了解影响的情况下做出决定。

那团队该咋做?他们咋确保他们的选择能最好服务于组织?幸好我们站在牛顿肩膀上,请遵循如下指导方针:

4.1 保持记录

若你从本文中只学到一件事,那必须是这点。看起来这应该是最容易做到的事情,但我见过的团队,很少能认真记录其技术决策。

我理解,文档很枯燥无趣,尤其当你专注于构建下一个大项目时。然而,它只需要很少时间,而清晰记录技术决策的巨大好处显而易见,它将不断地为你带来十倍、百倍、千倍回报。

每当面临技术决策并做任何重要选择时,记录何时及为何做出该选择。不需要是一份庞大的文件或复杂东西。

以下内容即可:

✅ 做出决定的时间

✅ 选择有哪些

✅ 每种选择的优缺点

✅ 为何选择特定选项

✅ 潜在担忧

在一个成熟团队/组织中,你可能需要使用更正式东西,如ADR(架构决策记录)。然而,对MVP级项目,上述内容足够。

这样的记录将允许你回顾并理解为何做出这些决策,并在以后防止混淆。它还将帮助你及早发现这些决策的任何陷阱。这引出下一要点:

4.2 定期重新审视你的决策

做出“正确”选择往往不如做出“有意识”选择重要,因为今天的“正确”选择可能是明年的“错误”选择。定期重审技术决策,重新评估,与团队一起头脑风暴,你将始终掌握组织技术状态的脉搏。

让这些成为定期的节奏。每月进行一两次会议。或每两个月一次会议。或每季度一次全天会议。无论哪种方式,重要的你要持续进行这些会议,并承诺将其作为定期的脉搏检查,以识别哪些技术解决方案仍然有效,哪些无效。

其实还有很多...

4.3 始终提出问题(尤其是当出现问题时)

质疑你的技术选择。根据公司目标进行预测,并查看这些目标如何与当前的技术状态对齐。是否能改进?咋改进?

在这里最有帮助的是对产品遇到的技术问题有敏锐意识。是否存在可扩展性问题?生产的错误率高吗?

用户等待20s才能加载网站?基础设施每周一、周四都崩溃?

这些问题是最好的指示,表明要做出改变了,并且需要重新审视和/或做出一些技术决策。

关注这些问题,记录它们,并询问咋解决它们(希望也能记录下你的发现)是开始这一过程的好方法。

5 结论

最终,你无法保证今天做出的决策或技术选择能永远为组织服务。在技术和业务需求不断变化的环境中,组织转向、市场趋势和其他因素的影响下,很难准确预测这些选择将如何塑造组织的未来技术状态。

然而,制定一个清晰、直观和简化的决策过程,可以优化这些决策,并在环境变化时提供调整和改变的途径。

这个过程将确保我们所做的架构权衡在我们所处的时间和条件下是正确的。

02-软件架构权衡-架构特性

本系列00文重点讨论啥是架构权衡以及为什么它们很重要。01文,阐述了在软件架构中进行有意识决策过程的必要性。

本文总结讨论具体的架构特性及为啥理解这些特性对系统至关重要。为做到这点,需理解:

  • 啥是架构特性
  • 选择关注某一特性有时会迫使我们在另一特性上做出权衡

换句话说,架构特性是指你的系统总体能力的具体方面,这就是架构的“ability”(属性),你很快就会明白为什么。

以下是一些常见的架构特性。这绝不是一个详尽的列表——只是一些你在设计和构建应用程序时最常遇到的特性,也是我在全职职业生涯和咨询实践中最常见的特性。

我试图在这里做的是识别特性,指出它在何时最有可能重要,关注它时我们在做什么权衡,以及一些实现该特性的方法。当然,这一切都是非常高层次的,每一个具体点上还有更多内容可以讨论。

1 架构特性

1.1 可审计性(Auditability)

这是啥?

你的系统在运行,事件在流转。用户启动动作并导致事情发生。许多情况下,你希望有一个明确记录,记录这些事情的发生。即你希望记录某个用户在特定时间发起了某个特定的动作,以及该动作的具体情况。

何时重要?

  • 金融等受监管行业
  • 遵守内部和外部标准(ISO、PCI、SOC、SOX)
  • 出于安全原因,以便你可以识别和调查未经授权的活动

我们在做啥权衡

  • 系统的复杂性增加
  • 测试变得更复杂,因为你需要测试审计事件在整个系统中是否被正确记录

一些实现方式

  • 将审计事件日志与应用日志分开记录。这可以存入数据库、独立日志流、第三方日志聚合系统或另一种永久云存储(例如AWS的S3)。
  • 制定明确的数据保留和清除策略(金融数据通常需要保留7年)。
  • 注意成本——大部分数据可以移到较少频繁使用的存储层(冷存储),因为这些数据可能不需要即时检索。

1.2 可观测性(Observability)

这是什么? 通常,人们对监控和可观测性之间的区别感到困惑。确实,这两个概念有很多重叠之处,常常被互换使用。一个常见的区分方式是,可观测性指的是在问题发生前识别系统潜在问题的能力。监控是通过日志、指标、跟踪、警报和仪表板查看系统内部发生的情况的能力。监控是实现可观测性的一部分。

在本文中,我将监控和可观测性都纳入可观测性的范畴。

何时重要?

任何实际系统都需要一定程度的可观测性。系统越复杂,使用频率越高,可观测机制就越复杂。

我们在做啥权衡

  • 可观测性增加系统复杂性
  • 根据可观测性的程度和部署的机制,它也可能影响应用性能
  • 成本——实现第三方/现成的解决方案 or 内部构建
  • 可扩展性——可观测系统需要随业务应用扩展。这是一个常见但易被忽视的问题,特别是内部/本地部署的可观测系统

一些实现方式

  • 使用云原生或第三方APM(应用性能监控)和可观测工具,如Datadog、New Relic、Dynatrace、Honeycomb等
  • 确保你的应用记录重要信息
  • 对分布式系统(如微服务)——使用跟踪来保持跨多个系统的请求的可见性
  • 使用AIOps工具(通常作为上述第三方解决方案的一部分)发现和预测应用行为
  • 使用OpenTelemetry等开放标准
  • 确保根据暴露的指标、日志和跟踪配置警报、仪表板、报告。换句话说,确保有可监控和实时操作的有意义的工件。

1.3 可伸缩性/弹性(Scalability/Elasticity)

这是什么?

应用程序支持增加的流量/更多请求或用户的能力。你的系统如何从每秒10次交易(TPS)调整到每秒1000次交易?当用户群从1万增长到100万时会发生什么?

在某些情况下,可伸缩性与弹性的概念可以互换使用,而在其他情况下,这两个概念表示系统的不同方面。准确地说:

  • 可伸缩性,指系统整体在较长时间内适应不断增长的负载的能力
  • 弹性,指系统在特定时间内适应波动负载/流量的能力

为简化,这里将这两个概念合并。

何时重要?

  • 如果你的应用预期会随着时间的推移而增长,需考虑可扩展性。这适用于大多数部署在生产环境中面向外部用户的系统
  • 唯一不需要担心可扩展性的情况是,如果你知道系统使用会被限制在一定范围内(例如内部公司应用)。这包括用户群已知非常小且不需要增长的生产应用

我们在做啥权衡

  • 系统的复杂性增加
  • 可部署性变得更困难。处理一个容器的部署要比处理1000个容器容易得多——即使你在使用容器编排框架。
  • 系统越可扩展,维护可观测性就越困难,因为需要观察、跟踪和调查的组件更多。
  • 可测试性可能更具挑战性,测试用例需要覆盖应用的分布式特性

一些实现方式

  • 使用容器编排系统(如Kubernetes)动态调整所需计算水平(容器数量)
  • 利用云环境中的无服务器服务,大部分扩展由云本身负责
  • 开发应用使其易于扩展。对于“单线程”运行时(如Node),意味着不要阻塞事件循环超过必要时间。对于多线程应用,意味着以最佳方式利用并发(避免死锁,使用非阻塞锁等)
  • 尽可能采用无状态范式,只有在必要时才维护和传递状态。
  • 通过频繁运行负载、压力和流量测试来识别瓶颈。
  • 优先扩展横向伸缩而非纵向伸缩

1.4 响应性(Responsiveness)

这是什么?

应用程序的响应能力是其在合理时间内对调用方提供某种响应的能力——即使在应用程序负载显著增加时。换句话说,应用能够处理并响应用户操作,而用户不会感到延迟。这适用于前端和后端应用。

何时重要?

  • 当应用是面向用户时——即实际有用户在等待某些事情发生。
  • 当你的用例对应用的响应性没有太多容忍度时。例如,某人在银行网站申请贷款,可能对延迟的容忍度更高(因为别无选择),而某人使用服务获取实时股票报价进行日间交易时,容忍度则较低。

做啥权衡

  • 通常,响应性与可扩展性直接相关。系统越可扩展,响应性越可能越好。然而,应用程序还需要通过设计确保响应性。
  • 应用程序的内部设计复杂性增加。
  • 可测试性变得更复杂,以覆盖可能限制响应性的潜在用例。
  • 在某些情况下,我们需要在响应性和正确性之间进行权衡。例如,考虑从服务A和服务B获取数据的情况,但服务B不可达。你可能希望返回服务A的数据,而不是无限期等待服务B,因为这会影响系统的响应性。这样做的结果是,你快速返回数据(来自服务A),但由于没有服务B的数据,响应不完整。

一些实现方式

  • 优雅降级
  • 断路器
  • 异步处理

1.5 容错性(Fault Tolerance)

这是什么?

系统在其一个或多个组件发生故障时继续运行的能力。

容错性也与应用响应性相关。区别在于,响应性更多是确保终端用户体验,而容错性关注的是系统在仅有部分组件工作的情况下,仍能继续运行并产生结果。

何时重要?

  • 任务关键系统

做啥权衡

  • 就像响应性一样,我们可能会为了系统整体的运行能力而牺牲系统输出的正确性。

一些实现方式

  • 优雅降级
  • 备用副本(计算和存储)
  • 主动/主动或主动/被动部署
  • 通过架构设计尽量减少关键组件的中断
  • 监控故障
  • 使用BASE事务代替ACID事务

1.6 可扩展性(Extensibility)

这是什么?

轻松向系统添加新功能和新集成的能力。

何时重要?

  • 当我们知道应用可能会以意想不到的方式增长和转变时
  • 当需要添加新组件时

做什么权衡

  • 当可扩展性是我们的目标时,需要提前进行大量规划,使系统架构易于扩展。

一些实现方式

  • 模块化软件——创建解耦、高内聚、封装的服务,彼此之间没有依赖(包括代码和架构)
  • 使用开放标准和最佳实践。例如,利用REST或流行的消息队列框架连接微服务
  • 使用清晰的API契约
  • 解耦数据存储/数据库,不同的领域数据使用不同的数据存储

1.7 可测试性(Testability)

这是什么?

系统能够轻松地进行手动和自动测试,无论是整体测试还是组件测试。

何时重要?

  • 当我们希望测试覆盖尽可能多的应用流程时

做什么权衡

  • 系统复杂性增加。尤其是对于分布式系统和异步流程,通常很难甚至不可能实现。
  • 系统需要设计和架构为允许组件进行隔离测试(包括代码和基础设施)。

一些实现方式

  • 可测试的代码(即模块化代码,便于单元测试、集成测试)
  • 模块化,使应用功能可以模拟
  • 确保尽可能多的测试可以自动化

1.8 性能(Performance)

这是什么?

性能与可扩展性和弹性密切相关,之间有很多重叠之处。区别在于,可扩展性和弹性通常指系统适应不断增长的负载的能力。而性能则讨论系统在合理时间内处理所有负载的能力。

何时重要?

  • 大多数面向用户的系统需要足够高的性能以提供积极的用户体验。什么是“足够高的性能”取决于应用的上下文。例如,对于大多数网站来说,超过几秒钟的响应时间通常被认为是糟糕的用户体验(尽管这取决于网站的功能)。

做什么权衡

确保性能需要系统在设计上能够预见并消除瓶颈。这还需要在关键点进行细致优化。这意味着需要投入足够的精力来设计系统,持续监控并识别问题区域。

一些实现方式

  • 非阻塞I/O
  • 异步编程和架构(消息传递、事件流)
  • 解耦系统
  • 优化长时间运行的过程和CPU密集型操作
  • 优化应用代码
  • 选择具有低网络开销的技术(即那些在堆栈较低协议上通信的技术,而不是HTTP)
  • 利用具有已知性能保证的云原生服务(只要这些服务按规定使用)

写在最后

公众号JavaEdge 专注分享软件开发全生态相关技术文章视频教程资源、热点资讯等,如果喜欢我的分享,给 🐟🐟 点一个 👍 或者 ➕关注 都是对我最大的支持。

欢迎长按图片加好友,我会第一时间和你分享软件行业趋势面试资源学习途径等等。

添加好友备注【技术群交流】拉你进技术交流群

关注公众号后,在后台私信:

  • 回复架构师,获取架构师学习资源教程
  • 回复【面试,获取最新最全的互联网大厂面试资料
  • 回复【,获取各种样式精美、内容丰富的简历模板
  • 回复 路线图,获取直升Java P7技术管理的全网最全学习路线图
  • 回复 大数据,获取Java转型大数据研发的全网最全思维导图
  • 更多教程资源应有尽有,欢迎关注并加技术交流群,慢慢获取

浏览 25
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报