当前位置: 首页 > news >正文

QCon演讲实录(下):多云管理关键能力实现与解析-AppManager

在上篇中,我们已经基本了解了多云管理。现在,我们将深入探讨多云管理关键能力实现:AppManager

什么是AppManager?

上面我们讲了理论、我们自己使用的交付流程和整体架构,下面我们进入关键能力实现与解析的环节,看看我们是如何实现上述这些能力的。

image.png

回到 AppManager 这个服务本身,它就是一个基于 OAM 的几种分离的角色,能够实现应用管理及交付的一个服务。

它因大数据侧业务诉求而生长,注重扩展能力、网络隔离环境交付、资源管理和版本管理。

扩展能力

首先来看最重要的扩展能力。不管在一开始的平台设计的时候有多么完善,都很难满足后续持续演进的业务需求。所以扩展性是一个 PaaS 平台的重中之重。

image.png

依托于 OAM 的设计,我们将所有的 Component / Addon / Trait / Policy / Workflow 等均做成了可插拔动态加载的 Groovy 脚本,并在此之上提供了插件管理及市场分发的能力。

图中的所有绿色的地方都是可根据业务需要自行扩展的,所有能力都可以在整个流程中动态引入、插拔和替换。

最上面基础设施研发的同学负责编写支持的组件类型、工作流类型、资源类型的 Groovy 脚本,并注册到 AppManager 内部。

中间的 SRE 同学负责编写 Trait、Policy 的 Groovy 脚本注册到 AppManager 内部,并负责配置环境及部署约束条件等信息。

下面的用户只需要把自己的应用代码写好,然后按照预先定义的 CI、CD、Watch 流程运行即可。

三类角色各司其职,共同高效的完成多云环境下整体应用的管理与交付过程。

图中所有绿色的地方扩展的 Groovy 脚本都是去实现预先定义的 Interface 抽象接口的,任何实现了这些 Interface 的 Groovy 脚本,都可以像搭积木一样被组装和替换到各个应用的生命周期中发挥自己的能力。包括构建、部署、销毁、状态监测。

在服务内部,我们为每个脚本定义了自己的 kind (类型) / name (名称) / revision (版本) 三元组,全局唯一。对于相同的 kind + name 组合,仅加载指定的单个版本的代码版本。通过这样的设计,实现不同用途,不同名称的脚本动态加载,且可以自由切换脚本代码版本。

扩展能力 – 插件包

虽然单个 Groovy 扩展方便且灵活,但不易分发,而开源版本 SREWorks 中社区用户的需求又千差万别,所以在此之上,我们增加了插件包的机制,插件包通过对一系列 Groovy 脚本的组合,实现对应功能的封装。用户只需要一键下载及安装即可实现对应的能力加载。

image.png

对于插件包本身,我们也定义了一套规范,用于社区用户进行二次开发编写自己需要的插件并上传到插件市场。

image.png

构建打包

如果我们回顾过去这些年来的交付介质演进,可以看到抽象层次是越来越高的。从最开始的 RPM 等系统层面的软件包,到 Docker 出现后的镜像,再到后面以镜像为基础,进行各种包装而产生的交付物,如 Helm Chart/Kustomize 等。

那么我们也不例外,在物理机时代,我们作为 SRE 去维护各种物理机的软件一致性以及在此之上交付各类服务,甚至还研发了配套的流程平台、作业平台、配置平台等等。在 Docker 普及之后,这项工作就简单了很多,只需要处理镜像即可,但镜像本身的管理及交付仍然是头疼的问题。

好在现在是云原生的时代,我们有各类 CRD+Operator,有完善及可靠的 Kubernetes 原语,更有事实上的打包交付的标准 Helm/Kustomize 来帮助我们更好的完成交付物的封装。接下来我们就看一下,我们在整个应用交付的流程中,如何处理好构建打包、制品管理及外部 CI 对接的。

image.png

通过图上我们可以看到,首先用户自定义的构建配置被提交,之后会被 AppManager 解析为应用包任务以及一个个的组件包任务,每个组件包任务会根据自己的组件类型选择对应的构建逻辑进行构建,这里可以有不同的数据源,也可以通过不同的方式产出镜像,不管是 Docker Daemon 还是 Kaniko 的方式都支持,也会推送到指定的镜像仓库中。最终完成后生成一个个组件包,以及拼装为一个应用包,赋予版本及标签后,最后上传到 OSS 或 MinIO 上进行制品存储。一个常见的用户场景是,通过标签声明当前制品的用途,在实际使用的时候通过标签进行过滤,并进而选择自己需要的版本号以确认唯一的制品。

当存在外部 CI 系统需要对接的时候,只需要将外部 CI 系统产出的镜像及其他 commit 信息通过 Trigger API 触发即可,后面的流程和上述人工主动触发一致。

应用部署

当获取到制品之后,就来到了整个系统中最核心也是最复杂的部分:应用部署。此处暂时先忽略多环境下的制品下发及相关细节,后面会单独讲到。现在先假设我们已经将制品传输到了指定环境中,并且镜像也已经在对应环境中可用,只是单纯的执行一次部署流程,会有哪些事情发生。

首先看一下整个 Application 的 Yaml,它完整定义了一个应用部署的模型,描述了一个应用应如何可靠而又灵活的交付到目标环境中。如前面所说,Component/Addon/Trait 为面向终态设计,Policy/Workflow 为面向过程设计。

image.png

之后来看一下整体的部署层级关系,从大到小分别是 Workflow 部署、Application 部署、Component 部署(含 Trait 部署)。当然并不局限于这三层,Workflow 是可以按业务需求进行各种嵌套的,后面会单独讲到。接下来我们用相反的顺序来介绍实现细节,自底向上,逐步拼出来我们最后的实现方案。

应用部署 - Trait

Trait 我们会归类为三种功能:

  • 根据配置修改绑定的 Component Workload Yaml。
  • 新增 / 修改资源 / 做你想做的事,并在完成后产出数据给自身对应的组件或后续其他组件使用。
  • 持续监听目标集群中当前组件的事件(不只是部署过程中),并实时根据事件做出反应(Groovy 实现),常用于根据组件的当前状态做出一些外部反应,比如 Pod 漂移 IP 变动后需要重新做一些信息注册或维护类的工作。

image.png

所有的 Trait 也都是 Groovy 脚本实现,并封装为插件包注册到系统中。每个 Trait Groovy 的 class 说明如上图,其中会包括两个 interface:

  • 一个是 execute,所有类型的 Trait 在部署过程中均会执行该方法,并产出数据供下游使用,当然产出数据是可选的
  • 另外一个是 reconcile,只有第三类需要持续监听目标集群事件的 Trait 才会使用,本质上是目标集群运行着我们开发的一个 Trait Operator,在 execute 执行的时候下发一个监听 CR 到该集群,Trait Operator 检测到该 CR 后会启动一个新的 Controller 来监听指定组件事件,并在事件到来后直接调用 AppManager 的 Trait Reconcile API,该 API 会直接同步调用对应 Trait Groovy 脚本中的 reconcile 方法,完成业务自定义的事件触发动作。这里其实等价于我们把常见的 K8S Operator 的 Reconcile 链路延长到了 AppManager 自身,并通过 Groovy 来实现业务逻辑,简化了常规 Operator 的开发流程,并且中心化也更好维护一些,尤其是面对茫茫多的目标集群的时候。

应用部署 - 组件

对于组件而言,我们定义了几个固定类型,分别是构建、部署和状态感知。其中构建前面介绍过了,状态感知是可选的,会在最后进行介绍。这里主要介绍下部署逻辑。

组件部署的时候,一样会执行对应的 Groovy 脚本,并且系统会在进入脚本执行前自动将选定的制品解压到本地目录供脚本使用,以执行业务自定义的部署逻辑。目前我们已经根据各个业务情况编写了 6 类内部使用的组件,这些因为和阿里云业务相关无法开源。在开源场景下,我们默认提供了微服务、Job、Helm 几种类型,并开放了扩展机制,供有需要的用户自行编写新的组件类型来控制自己的组件如何部署。这里的组件类型是抽象的,也可以完全和 K8S 无关,比如是自己公司内部系统的某些特定配置导入导出。

image.png

由上面的图可以看到,在部署过程中,Groovy 脚本中的 launch 方法会首先被调用,并完成对应的部署流程,这里可通过 fabric8 进行 K8S 远程调用 apply 下去对应的 Yaml,当完成后立即返回即可。之后,AppManager 服务会不停轮询当前 Groovy 脚本中的 get 方法,期望查询到成功的返回值,否则就会一直等下去,直到超时或失败。

这里在检查业务组件终态的时候其实还可以和组件自身的状态感知的能力产生互动,比如一直没有终态,但是没有终态的原因是什么呢?是可以通过后面说的状态感知来分析失败原因并返回给用户的。后面我们再细讲。

应用部署 – 应用 & 组件 & Trait 间依赖关系

说完了组件和 Trait 的部署,我们上升到应用层面,来看下在应用的视角下,自身的组件、Trait 在整个部署流程中的数据关系。

因为实际的业务场景中,每个组件和 Trait 都有可能产生数据输出,并且会被别的组件和 Trait 引用到,在这种依赖关系下,产出数据的组件不跑完,引用数据的组件是不能跑的。但如果两个组件间没有任何依赖关系,那么他们是需要并行跑来提速的。

image.png

所以按照 OAM spec 的描述,我们在 Application Yaml 中为每个组件和 Trait 都提供了 dataInputs 和 dataOutputs 能力,如图上的 Yaml 所示。

在 AppManager 系统收到上面的 Yaml 后,在整个应用部署的过程中就需要对针对所有的组件和 Trait 之间的关系建图连边了。

image.png

在构图的时候,Component 和 Trait 是一视同仁的,这种对等关系会降低复杂度,同时保证数据流依赖关系在两种类型上保持一致。

对于每个 Component,会单独再构造一个镜像节点,用于确保前置组件的后置 Trait 节点的运行在当前组件之前。这样整体构图之后,只需要按照这个 DAG 无脑执行即可满足约束顺序。

应用部署 -Workflow

在早期的业务支撑中,其实是没有 Workflow & Policy 这一层的,但是上面讲到的应用部署只是针对单目标的,如果是批量目标环境的部署,就需要在更高的纬度发起多个应用部署单子来解决。

OAM 针对这一情况也在 0.3.1 的草案中提出了解决方案,也就是 Workflow & Policy。这里简要说明一下:

  • 一个 Workflow 包含多个步骤,执行顺序可以顺次,也可以并发,如果并发的话就是按照上面说的 dataInputs/dataOutputs 的 DAG 构造方式。
  • 每个 Workflow Step 也都是 Groovy 脚本来实现,所以每个步骤执行什么,怎样执行全部交给平台方来定义。
  • 每个 Workflow Step 的 Groovy 脚本中,可以调用 Policy 对全局 Application 进行自定义修改,并且将修改后的 Application 提交应用部署单,也就是前面说的逻辑,这样就可以实现不同目标环境有不同的覆写配置。
  • Workflow 在执行的过程中可以有更多的控制及人工介入,如暂停、恢复、设置上下文等等,在灰度发布场景、涉及回滚动作的时候尤其有用。
  • Workflow Step 可以继续产出一个 Workflow,实现复杂场景实现套娃效果。

资源管理(Addon)

秉承着所有可扩展的东西都通过 Groovy 脚本来插拔,资源 Addon 也不例外,其实也可以把它看做是 Component 组件。

对于 Addon,会有和 Component 不同的语义。资源需要申请和释放,也就是 apply 和 release,所以这两个方法是 Addon 层面的 interface。同时 Addon 也会存在自己的规格 Schema 定义,前端会在资源申请的时候识别这个定义,让用户在页面选择需要申请的资源规格(比如 8c16g 的数据库)。

在我们的实践过程中,直接写了一个对接 Terraform 的 Groovy 脚本就完成了大部分的内部业务诉求。对于所有的 tfstate 文件也会透出以 Addon Schema 的形式存储到数据库中供后续其他组件使用或共享。

image.png

注意资源的特性是应用下根据 Namespace 隔离的,但 Namespace 下的同名资源会进行共享。一个典型的场景是生产的 Namespace 下申请的数据库会共享到预发和生产两个环境的服务中。这里在下一页的多环境支持中进行介绍。

如果你对于其他第三方的各类异构资源有需求,比如有的资源需要通过特定的 API 申请的,完全可以再扩展一个插件注册进去即可实现。

多环境支持

image.png

我们在 AppManager 中定义了四种环境层级,如上图。从上到下分别是:

  • Unit(单元):单元间网络隔离。每个单元需要一个 AppManager 实例进行管控。一个单元可包括多个 Cluster。
  • Cluster(集群):一个独立 K8S 集群,集群间网络可达。集群直接注册 kubeconfig 到当前单元下的 AppManager 即可使用。
  • Namespace(命名空间): 对应 K8S 的 Namespace 概念,用于一个 Cluster 下的资源及应用的隔离,一个 Namespace 对应了一个信息孤岛。
  • Stage(阶段/环境): 一个 Namespace 可包含多个 Stage,每个 Stage 共享了当前 Namespace 下的所有资源。

应用状态感知

一个应用被交付到目标环境之后,运行的状态怎么样?这块其实有很多种外部开源方案或监控手段可以来搞的,不展开。这里只介绍下 AppManager 内置的一个简单的状态感知方案。

image.png

由上图可以看到,一个应用被部署到了一个目标环境中,就变成了一个应用实例 Application Instance。一个组件被部署后也一样会变成组件实例 Component Instance。应用实例的状态是由组件实例的状态汇聚而来的,那么组件实例的状态需要怎么来呢?

首先来看 Watch 的方案,时效性高,但支撑的集群数量只有几百个,小规模下使用。本质上是在组件注册的时候通过 Informer 机制 List/Watch 集群事件,然后将 Event Handler 的实现交由组件开发者自己自行判断,并根据内容返回业务自定义的状态结果。

image.png

接下来是 Pull 方案,可以支撑最高 10 万量级的应用及集群,但时效性在应用状态正常时会逐步降低到 5min。只有在发生异常时会快速提升检测频率,直到再次恢复正常。当然具体的状态判定的脚本也是交给组件开发者来写的,可以根据业务需要自行扩展,灵活性非常高。

image.png

以上为本次分享全部内容,谢谢大家的聆听。

相关文章:

QCon演讲实录(下):多云管理关键能力实现与解析-AppManager

在上篇中,我们已经基本了解了多云管理。现在,我们将深入探讨多云管理关键能力实现:AppManager。 什么是AppManager? 上面我们讲了理论、我们自己使用的交付流程和整体架构,下面我们进入关键能力实现与解析的环节&…...

刚刚退出了一个群,关于在要麒麟OS上运行Labview

年龄过了45,看问题,与以前不太一样了。 觉得浪费时间的事,宁可发呆,也不会参和。 竟然一个群里在讨论如何满足客户的需求:麒麟OS上运行Labview。 然后直接退了群。 这种问题,我觉得可能 发在csdn上&…...

el-uploader 文件上传后,又被修改,无法提交到后端 ERR_UPLOAD_FILE_CHANGED

problem 文件上传后,又被修改,无法提交到后端 具体步骤: 文件上传本地文件打开并修改保存提交ajax 这个问题不仅仅局限于el-uploader,是一个普遍性的问题 导致的问题 问题1:提交请求时,控制台报错 net…...

利用Eigen实现点云体素滤波

目录 前言 一、算法原理 二、代码实现 1.头文件 2.源文件 三、效果展示 前言 体素滤波原理简单,是常用的...

linux高级命令之多进程的使用

多进程的使用学习目标能够使用多进程完成多任务1 导入进程包#导入进程包import multiprocessing2. Process进程类的说明Process([group [, target [, name [, args [, kwargs]]]]])group:指定进程组,目前只能使用Nonetarget:执行的目标任务名…...

CSS 圆角边框 盒子阴影 文字阴影

目录 1.圆角边框(重点) 2.盒子阴影(box-shadow) 3.文字阴影(text-shadow) 1.圆角边框(重点) border-radius 属性用于设置元素的外边框圆角。 语法: border-radius: l…...

python简单解析打印onnx模型信息

当我们加载了一个ONNX之后,我们获得的就是一个ModelProto,它包含了一些版本信息,生产者信息和一个GraphProto。在GraphProto里面又包含了四个repeated数组,它们分别是node(NodeProto类型),input(ValueInfoProto类型)&a…...

UE4 编写着色器以及各种宏的理解

参考链接:如何为 UE4 添加全局着色器(Global Shaders) - Unreal Enginehttps://docs.unrealengine.com/5.1/zh-CN/adding-global-shaders-to-unreal-engine/如何为 UE4 添加全局着色器(Global Shaders) - Unreal Engin…...

小笔记:Python 使用字符串调用函数

小笔记:Python中如何使用字符串调用函数/方法?jcLee95:https://blog.csdn.net/qq_28550263?spm1001.2101.3001.5343 本文地址:https://blog.csdn.net/qq_28550263/article/details/111874476 邮箱 :291148484163.co…...

红黑树的原理+实现

文章目录红黑树定义性质红黑树的插入动态效果演示代码测试红黑树红黑树 定义 红黑树是一个近似平衡的搜索树,关于近似平衡主要体现在最长路径小于最短路径的两倍(我认为这是红黑树核心原则),为了达到这个原则,红黑树所…...

用于非线性时间序列预测的稀疏局部线性和邻域嵌入(Matlab代码实现)

💥💥💞💞欢迎来到本博客❤️❤️💥💥 🏆博主优势:🌞🌞🌞博客内容尽量做到思维缜密,逻辑清晰,为了方便读者。 ⛳️座右铭&a…...

使用 Vue3 重构 Vue2 项目

目录前言:一、项目整体效果展示二、项目下载使用方法三、为什么要重构项目四、重构的流程五、步骤中的 bug 以及解决方式六、未解决的问题总结:前言: 2020年9月18日,vue3正式版发布了,前几天学习完成后,我决…...

Hive学习——单机版Hive的安装

目录 一、基本概念 (一)什么是Hive (二)优势和特点 (三)Hive元数据管理 二、Hive环境搭建 1.自动安装脚本 2./opt/soft/hive312/conf目录下创建hive配置文件hive-site.xml 3.拷贝一个jar包到hive下面的lib目录下 4.删除hive的guava,拷贝hadoop下的guava 5…...

uprobe 实战

观测数据源 目前按照我的理解,和trace相关的常用数据源–探针 大致分为四类。 内核 Trace point kprobe 用户程序 USDT uprobe 在用户程序中,USDT是所谓的静态Tracepoint。和内核代码中的Trace point类似。实现方式是在代码开发时,使用USDT…...

华为OD机试 - 求最大数字(Python)| 真题+思路+考点+代码+岗位

求最大数字 题目 给定一个由纯数字组成以字符串表示的数值,现要求字符串中的每个数字最多只能出现2次,超过的需要进行删除;删除某个重复的数字后,其它数字相对位置保持不变。 如34533,数字3重复超过2次,需要删除其中一个3,删除第一个3后获得最大数值4533 请返回经过删…...

雨水情测报与大坝安全监测系统

压电式雨量传感器产品概述传感器由上盖、外壳和下盖组成,壳体内部有压电片和电路板,可以固定在外径50mm立柱上和气象站横杆上。传感器采用冲击测量原理对单个雨滴重量进行测算,进而计算降雨量。雨滴在降落过程中受到雨滴重量和空气阻力的作用…...

抖音广告投放形式有哪些?新品牌进入抖音怎么建立口碑

坐拥5亿用户的抖音平台,已经成为各大品牌的兵家必争之地。想要在这块宣传的“高地”,做出声量,就必须了解抖音广告投放形式有哪些。这里整理的这份抖音广告投放指南,你一定不能错过。一、抖音为何如此牛想要弄清楚抖音广告的投放形…...

Beefxss使用教程图文教程(超详细)

「作者主页」:士别三日wyx 「作者简介」:CSDN top100、阿里云博客专家、华为云享专家、网络安全领域优质创作者 Beefxss一、首次使用二、修改账号密码三、自带练习页面四、简单使用五、工具界面介绍六、功能演示1、网页重定向2、社工弹窗3、功能颜色标识…...

【Python学习笔记】35.Python3 CGI编程(2)

前言 本章继续介绍Python的CGI编程。 通过CGI程序传递checkbox数据 checkbox用于提交一个或者多个选项数据&#xff0c;HTML代码如下&#xff1a; 实例 <!DOCTYPE html> <html> <head> <meta charset"utf-8"> <title>csdn教程(csd…...

博客等级说明

CSDN 博客等级是按照用户的博客积分数量进行的设定&#xff0c;为 Lv1 至 Lv10 共 10 个等级&#xff0c;不同的等级创作者可以享受到不同的权益待遇。例如&#xff0c;皮肤奖励、自定义域名、客服优先处理、自定义文章标签等特权。您需要提高博客积分进一步提升等级&#xff0…...

装饰模式(Decorator Pattern)重构java邮件发奖系统实战

前言 现在我们有个如下的需求&#xff0c;设计一个邮件发奖的小系统&#xff0c; 需求 1.数据验证 → 2. 敏感信息加密 → 3. 日志记录 → 4. 实际发送邮件 装饰器模式&#xff08;Decorator Pattern&#xff09;允许向一个现有的对象添加新的功能&#xff0c;同时又不改变其…...

label-studio的使用教程(导入本地路径)

文章目录 1. 准备环境2. 脚本启动2.1 Windows2.2 Linux 3. 安装label-studio机器学习后端3.1 pip安装(推荐)3.2 GitHub仓库安装 4. 后端配置4.1 yolo环境4.2 引入后端模型4.3 修改脚本4.4 启动后端 5. 标注工程5.1 创建工程5.2 配置图片路径5.3 配置工程类型标签5.4 配置模型5.…...

云计算——弹性云计算器(ECS)

弹性云服务器&#xff1a;ECS 概述 云计算重构了ICT系统&#xff0c;云计算平台厂商推出使得厂家能够主要关注应用管理而非平台管理的云平台&#xff0c;包含如下主要概念。 ECS&#xff08;Elastic Cloud Server&#xff09;&#xff1a;即弹性云服务器&#xff0c;是云计算…...

FastAPI 教程:从入门到实践

FastAPI 是一个现代、快速&#xff08;高性能&#xff09;的 Web 框架&#xff0c;用于构建 API&#xff0c;支持 Python 3.6。它基于标准 Python 类型提示&#xff0c;易于学习且功能强大。以下是一个完整的 FastAPI 入门教程&#xff0c;涵盖从环境搭建到创建并运行一个简单的…...

AtCoder 第409​场初级竞赛 A~E题解

A Conflict 【题目链接】 原题链接&#xff1a;A - Conflict 【考点】 枚举 【题目大意】 找到是否有两人都想要的物品。 【解析】 遍历两端字符串&#xff0c;只有在同时为 o 时输出 Yes 并结束程序&#xff0c;否则输出 No。 【难度】 GESP三级 【代码参考】 #i…...

【解密LSTM、GRU如何解决传统RNN梯度消失问题】

解密LSTM与GRU&#xff1a;如何让RNN变得更聪明&#xff1f; 在深度学习的世界里&#xff0c;循环神经网络&#xff08;RNN&#xff09;以其卓越的序列数据处理能力广泛应用于自然语言处理、时间序列预测等领域。然而&#xff0c;传统RNN存在的一个严重问题——梯度消失&#…...

HTML 列表、表格、表单

1 列表标签 作用&#xff1a;布局内容排列整齐的区域 列表分类&#xff1a;无序列表、有序列表、定义列表。 例如&#xff1a; 1.1 无序列表 标签&#xff1a;ul 嵌套 li&#xff0c;ul是无序列表&#xff0c;li是列表条目。 注意事项&#xff1a; ul 标签里面只能包裹 li…...

Java入门学习详细版(一)

大家好&#xff0c;Java 学习是一个系统学习的过程&#xff0c;核心原则就是“理论 实践 坚持”&#xff0c;并且需循序渐进&#xff0c;不可过于着急&#xff0c;本篇文章推出的这份详细入门学习资料将带大家从零基础开始&#xff0c;逐步掌握 Java 的核心概念和编程技能。 …...

CRMEB 框架中 PHP 上传扩展开发:涵盖本地上传及阿里云 OSS、腾讯云 COS、七牛云

目前已有本地上传、阿里云OSS上传、腾讯云COS上传、七牛云上传扩展 扩展入口文件 文件目录 crmeb\services\upload\Upload.php namespace crmeb\services\upload;use crmeb\basic\BaseManager; use think\facade\Config;/*** Class Upload* package crmeb\services\upload* …...

Hive 存储格式深度解析:从 TextFile 到 ORC,如何选对数据存储方案?

在大数据处理领域&#xff0c;Hive 作为 Hadoop 生态中重要的数据仓库工具&#xff0c;其存储格式的选择直接影响数据存储成本、查询效率和计算资源消耗。面对 TextFile、SequenceFile、Parquet、RCFile、ORC 等多种存储格式&#xff0c;很多开发者常常陷入选择困境。本文将从底…...