颍上新闻,颍上资讯

您当前的位置:颍上人才网 >> 颍上资讯 >> 面试技巧 >> 微服务架构下全链路监控组件的产生背景及面临的问题
微服务架构下全链路监控组件的产生背景及面临的问题
2026-01-30|资讯来源: 网络整理|查看: 112

点击上方码猿技术专栏?轻松关注,设为星标!

及时获取有趣有料的技术

跟随微服务架构流行起来,服务按照各异维度拆分,一次请求常常动辄关联多个服务。互联网应用构建于不同软件模块集合,但这些软件模块,存在由不同团队开发的可能,存在用不同编程语言达成的可能,还有可能部署于数千台服务器,跨越多个不同数据中心。所以,需要一些能辅助理解系统行为、用来剖析性能问题的工具,以便在故障出现之时,能够迅速定位并解决该问题。

存在这样的问题背景,全链路监控组件由此产生。其中最出名的,是谷歌公开的论文里提及的Google Dapper。若要在这个上下文里理解分布式系统的行为,就得监控那些跨越不同应用、不同服务器之间的关联动作。

因此,在包含复杂度的微服务架构体系当中,几乎每一回前端发出的请求,都会造就出一组含有复杂度的分布式服务调用衔接线路。一条请求完整的调用链条,或许类似于下方所展示的图形那样:

一个请求完整调用链

那么,在业务规模持续性增大的状况下,在服务数量不断增多的情形下,在频繁进行变更的状况中,并依据复杂的调用链路,随之带来了一系列问题:

如何快速发现问题?

如何判断故障影响范围?

如何梳理服务依赖以及依赖的合理性?

如何分析链路性能问题以及实时容量规划?

与此同时,我们会留意于请求进行处理这个期间之内,关于各个方面调用的各种性能指标,举例来说,像是吞吐量,也就是TPS,还有响应时间,以及错误记录等等。

依据拓扑,针对相应组件,能够计算其实时吞吐量,对于平台,也能计算其对应的实时吞吐量,物理设备同样如此,可计算出其实时吞吐量。

响应时间,包括整体调用的响应时间和各个服务的响应时间等。

错误记录,根据服务返回统计单位时间异常次数。

全链路性能监控,能从整体层面至局部层面呈现各项指标,会把跨应用的全部调用链性能信息集中予以展示,如此一来便能便利地对整体性能与局部性能展开度量,而且还能便利地探寻到故障产生的根源所在,在生产环节上能够极大程度地缩减故障排除所耗费的时间。

有了全链路监控工具,我们能够达到:

对请求链路展开追踪,实现故障的快速定位,能够借助调用链并依靠业务日志迅即定位错误信息。

可视化:各个阶段耗时,进行性能分析。

依赖优化:各个调用环节的可用性、梳理服务依赖关系以及优化。

为了能够得出用户的行为路径,进而汇总分析应用于众多业务场景,要进行数据分析,还要同步优化链路。

既然是像前面所说的那样,那接下来要考虑这么回事,咱去挑那种全链路监控组件的时候,会有什么样的目标要求呢?Google Dapper 中间好像也提到了相关的状况呀,把这些状况总结起来内容会是这样,如下逐个罗列:

探针的性能消耗

代码的侵入性

可扩展性

数据的分析

一般的全链路监控系统,大致可分为四大功能模块:

埋点与生成日志

收集和存储日志

分析和统计调用链路数据,以及时效性

展现以及决策支持

存在基本工作单元,有一次链路调用,此链路调用不限于特定类型(比如 RPC、DB 等)时会创建一个 span,该 span 由一个 64 位 ID 来标识,使用 uuid 较为便利,span 当中含有其他数据,诸如描述信息、时间戳、以 key-value 对形式存在的(Annotation)tag 信息、parent_id 等,其中 parent-id 能够表明 span 调用链路的来源。除此以外,留意公众号码猿技术专栏,回应关键词“面试宝典”,免费得到Java经典面试宝典。

一次大的跟踪过程里,span呈现出的样子由上图说明。Dapper记录了span所具有的名称,还记录了每个span具备的ID以及父ID,目的是凭此重建一次追踪过程中不同span之间存在的关系。若一个span不存在父ID便被称作root span。所有span都挂靠于一个特定的跟踪,并且共用一个跟踪id。

Span 数据结构:

type Span struct {
    TraceID    int64 // 用于标示一次完整的请求 id
    Name       string
    ID         int64 // 当前这次调用 span_id
    ParentID   int64 // 上层服务的调用 span_id  最上层服务 parent_id 为 null
    Annotation []Annotation // 用于标记的时间戳
    Debug      bool
}

Span集合,其类似树结构,则展现一次全然完整的跟踪,此跟踪起始于请求至服务器启动之时,终结于服务器返回response之际,这般模式对每次rpc调用耗时作跟踪记录,并且存有独一无二用以标识的trace_id。譬若呈现为:你所运行的分布式大数据存储,一次Trace便由你的一回请求构建而成。

每种颜色的note,标注了一个span,一条链路,通过TraceId唯一标识,Span标识发起的请求信息。树节点,是整个架构的基本单元,而每一个节点,又是对span的引用。节点之间的连线,表示的span和它的父span直接的关系。虽然span在日志文件中,只是简单的代表span的开始和结束时间,他们在整个树形结构中,却是相对独立的。

注释,用以记载与请求特定事件有关联的信息(像是时间),在一个 span 当中会存在多个 annotation 注释得以详述描述。通例会涵盖四个注释层面表现的信息:

cs:Client Start,表示客户端发起请求

sr:Server Receive,表示服务端收到请求

服务端完成处理,这一行为被称作Server Send,它意味着要把结果发送给客户端。

cr,也就是Client Received所表达的意思,指,客户端获取到服务端返回信息。

Annotation 数据结构:

type Annotation struct {
    Timestamp int64
    Value     string
    Host      Endpoint
    Duration  int32
}
?

调用示例

请求调用示例

设若用户发起一项请求,此请求最先抵达前端 A 服务,接着分别针对于 B 服务以及 C 服务展开 RPC 调用。

B服务处理完毕后,会给A做出响应,然而,D服务与E服务要和后端的C服务进行交互,之后返还给A服务,最终,由A服务响应用户的请求。

调用过程追踪

当请求来临之际,会生成出一个全局性质的 TraceID,借助这个 TraceID 能够将整个调用链进行串联,而一个 TraceID 所代表的乃是一次请求。

之外,除了 TraceID ,还需要 SpanID 用来记录调用父子关系,每个服务会记录下 parent id 以及 span id ,籍由它们能够组织一次完整调用链的父子关系。

一个 span,如果它没有 parent id,那么它就会成为 root span,而这能够被看成是调用链的入口。

所有这些 ID 可用全局唯一的 64 位整数表示;

整个调用的过程当中,每一个请求都需要进行透传,透传的内容是TraceID,还有SpanID。

针对每次该请求所附带的 TraceID,以及附带的 SpanID,每个服务都把它们当作 parent id 予以记录,并且,每个服务还会记录下自身生成的 SpanID。

欲查看某次具备完整性的调用情况,那么,只需依据TraceID去找出全部调用记录,接着,凭借parent id以及span id将整个调用之时呈现的父子关系构建起来。此外,关注名为号猿技术专栏的公众号,回复关键词“面试宝典”,便能免费获取Java经典面试宝典。

调用链核心工作

开展调用链数据的生成工作,针对整个调用过程里的全部应用实施埋点操作,进而输出日志。

调用链数据采集,对各个应用中的日志数据进行采集。

将调用链数据予以存储外加查询,针对采集得来的数据实施存储行为,鉴于日志数据的量通常来讲都特别大,不仅得能够对其予以存储,并且还需要具备提供快速查询的能力。

采集日志数据,进行各种指标的运算,把运算结果保存起来,用于存储及查询指标运算。

告警功能,提供各种阀值警告功能。

整体部署架构

通过 AGENT 生成调用链日志。

通过 logstash 采集日志到 kafka。

kafka 负责提供数据给下游消费。

storm 计算汇聚指标结果并落到 es。

storm将trace数据抽取出来并落到es之中,这么做是为了提供较为复杂的查询,举例来说,借助时间维度查询调用链,能够快速查询出所有符合条件的traceID,依据这些traceID去Hbase查数据那就快些了。

logstash把kafka的原始数据拽取到hbase 里头,traceID是其的rowkey啊,依据traceID去查询那效率是十分迅速了。

AGENT 无侵入部署

将性能测量和运维成本减、通过去掉侵入式,以 AGENT 代理来部署,能把业务逻辑与性能测量完全分离,所以能做到测量任意类里任意方法的执行时间,这样的行为方式使得采集效率大幅度提升。按照服务跨度来划分,AGENT 主要是两大类:

Dubbo 支持;

Rest 支持;

自定义 RPC 支持;

调用链监控好处

准确掌握生产一线应用部署情况;

从调用链全流程性能角度,识别对关键调用链,并进行优化;

提供可追溯的性能数据,量化 IT 运维部门业务价值;

快速定位代码性能问题,协助开发人员持续性的优化代码;

协助开发人员进行白盒测试,缩短系统上线稳定期;

4方案比较

分布式系统调用链路分析_分布式事务面试_全链路监控组件

市面里所存在的全链路监控理论模型之中大部分都是去借鉴 Google Dapper 论文,在这篇文章里重点关注以下三种 APM 组件, 其一是,其二是,其三为。

以上三种全链路监控方案需要对比的项提炼出来:

探针的性能

collector 的可扩展性

全面的调用链路数据分析

对于开发透明,容易开关

完整的调用链应用拓扑

探针的性能

所较为关注的是探针的性能,毕竟 APM 定位作为工具,要是启用链路监控组件之后,直接致使吞吐量降低超过一半,那同样是无法接受的。针对 skywalking、zipkin、pinpoint 展开了压测,并且将其与基线(未运用探针)的情形予以了对比。

选取了一个平常的、基于Spring的应用程序,它涵盖Spring Boot、Spring MVC、redis客户端以及mysql。对这个应用程序进行监控,每一个trace,探针会抓取5个span(分别是1个Tomcat、1个SpringMVC、2个Jedis、1个Mysql)。这里基本上与skywalkingtest的测试应用没什么差别。

模拟了三种情况,第一种情况并发用户为500,第二种情况并发用户为750,第三种情况并发用户为1000。采用jmeter进行测试,首先每个线程要发送30个请求,并且设置思考时间为10毫秒。所使用的采样率是1,也就是100% ,不过这边和生产情况可能存在差别。pinpoint默认的采样率是20,也就是50% ,通过设置agent的配置文件更改成了100%。zipkin默认同样是1。把这些情况组合在一起,总共是12种。下面来看一下汇总表。

探针性能对比

于上表能看出,于三种链路监控组件里,skywalking的探针针对吞吐量所产生的影响是最小的,zipkin的吞吐量处于中间位置。pinpoint的探针针对吞吐量的影响相对比较明显,在有着500并发用户的情况下,测试服务的吞吐量从1385下降到了774,其影响程度很大。接着再去查看一下CPU和memory的影响情况,在内部服务器所开展的压测,针对CPU和memory的影响大致都在10%以内。

collector 的可扩展性

收集器具备的可扩展性,致使其能够朝着水平方向进行扩展,进而达成对大规模服务器集群的支持。

zipkin

skywalking

支持两种部署方式的 skywalking 的 collector,分别是单机模式与集群模式,collector 跟 agent 之间的通信运用了 gRPC。

pinpoint

照样,pinpoint同样是对集群以及单机部署予以支持的。pinpoint agent借助thrift通信框架,朝着collector传输链路信息。

全面的调用链路数据分析

完整的调用链路数据开展分析,给出代码层面的可见性,借此能够轻易地定位失败之处以及瓶颈所在。

zipkin

相对而言,zipkin 的链路监控在粒度方面没有着实显得细微,能够从上面呈现的图示看出,在调用链里,具体细致到了接口的层级,然而,再进一步深入的、关于调用环节过程的信息却并未有所涉及。

skywalking

Skywalking 同样对 20 余种中间件、框架以及类库予以支持,举例来说,涵盖主流的 Dubbo、Okhttp,另外还包括 DB 与消息中间件。上面所呈现的 Skywalking 链路调用分析截取部分相对简易,网关对 user 服务发起调用,鉴于其对诸多中间件具备支持能力,故而 Skywalking 链路调用分析相较于 Zipkin 更为完备一些。

pinpoint

在这三种APM组件中,pinpoint应当是那个数据分析最为周全完备的组件。它能提供代码级别的可得见性,借此便可轻松地去定位失败点以及瓶颈所在。就如从上面的图里能够看到那样,对于执行的那些sql语句,全都进行记录了。而且,它还能够配置报警规则之类的,去设置每个应用所对应的负责人,依据配置好的规则执行报警操作,其支持的中间件以及框架也是比较齐全完备的。

对于开发透明,容易开关

我们期望功能在不修改代码的情况下就能工作嘛,而且还希望能得到代码级别的可见性,对于开发要透明,开关得容易操作,增加的新功能无需通过修改代码来达成,启用或者禁用也得简便易行。

关于这一点,Zipkin借助修改后的类库还有其自身的容器(Finagle)来达成分布式事务跟踪功能。然而,其存在一个要求即于有需要的情况下对代码予以修改。skywalking以及pinpoint均是以基于字节码增强这种方式来实现的,开发人员并不需要去修改代码,而且借助字节码内更多的信息能够收集到数量更多且更为精确的数据。

完整的调用链应用拓扑

自动检测应用拓扑,帮助你搞清楚应用的架构。

pinpoint 链路拓扑

skywalking 链路拓扑

zipkin 链路拓扑

上面的那三幅图,各自分别展示了APM组件的调用拓扑,每一个都能够实现完整的调用链应用拓扑。相对而言,pinpoint的界面显示得更为丰富,具体到调用的DB名,zipkin的拓扑局限于服务与服务之间。

Pinpoint 与 Zipkin 细化比较

Pinpoint 与 Zipkin 差异性

Pinpoint是一种完整的性能监控解决办法,它涵盖从探针、收集器、存储一直到Web界面等一整套体系,而Zipkin仅仅着重于收集器以及存储服务,虽说它也具备用户界面,然而其功能和Pinpoint根本没有办法相提并论。反倒Zipkin提供了Query接口,具备更为强大的用户界面以及系统集成能力,能够依据该接口通过二次开发来达成。

Zipkin官方所提供的,是基于Finagle框架,且此框架是Scala语言的接口,至于其他框架的接口,则是由社区作出贡献,当下所能支持的,有Java、Scala、Node、Go、Python、Ruby以及C#等主流开发语言以及框架;然而Pinpoint目前有的,仅仅是官方所提供的Java Agent探针,其他的均处于请求社区支援的状态之中(请参见#1759和#1760)。

Pinpoint 给出 Java Agent 探针,借由字节码注入的办法达成调用拦截以及数据收集,能够达成真正的代码无侵入,只在启动服务器之际添加些许参数,便可完成探针的部署,Zipkin 的 Java 接口实现 Brave,仅给出基本的操作 API,若要与框架或者项目集成,就得手动添加配置文件或者增加代码。

用于精确查找的工具Pinpoint的后端存储是基于HBase的,然而,名为Zipkin的工具却是基于Cassandra的。

Pinpoint 与 Zipkin 相似性

Pinpoint基于Google Dapper的那篇论文,Zipkin同样基于Google Dapper的那篇论文,所以它们的理论基础粗略来讲是一样的。Pinpoint会把服务调用分解成若干存在级联关系的Span,这些Span通过SpanId和ParentSpanId来实现调用关系的级联,Zipkin也是如此。最后,Pinpoint会把整个调用链所流经的全部Span聚合成一个Trace,将其报告给服务端的collector去进行收集以及存储,Zipkin同样会这样做。

即使是在这一要点上,Pinpoint所运用的概念并不能够做到完全准确契合那一篇论文。举例来说,他选用TransactionId去替换TraceId,然而真正的TraceId却是一种结构,在这个结构当中涵盖了TransactionId、SpanId以及ParentSpanId。并且,Pinpoint 在 Span 的下方,增添了一个 SpanEvent 结构,该结构用于记录一个 Span 内部的调用细节,诸如具体的方法调用等,所以,Pinpoint 默认情况下会比 Zipkin 记录更多的跟踪数据。可是,从理论层面来讲,并未对Span的粒度尺寸作出限定,因而,一次服务调用能够是一个Span,如此一来,每个服务里的方法调用同样能够是一个Span ,照此情形,实际上,Brave也能够追踪到方法调用的级别,仅仅是在具体实现方面未曾这般去做罢了。

字节码注入 vs API 调用

Pinpoint达成了基于字节码注入的Java Agent探针,Zipkin的Brave框架只是给出了应用层面的API,然而仔细琢磨问题绝非如此简单。字节码注入是一种简单且粗暴的解决办法,从理论来讲不管何种方法调用,都能够借由注入代码的方式达成拦截,也就是说不存在实现不了的情况,唯有不会去实现的区别。但Brave并非如此,它所提供的应用层面的API还得有框架底层驱动的支撑,方可实现拦截。比如说,MySQL的JDBC驱动,它提供了注入interceptor的方法,仅需实现StatementInterceptor接口,接着在Connection String里进行配置,便能很简便地实现相应拦截;然而与之相反的是,低版本的MongoDB驱动或者Spring Data MongoDB的实现并没有这样的接口,若要实现拦截查询语句的功能,就会比较艰难。

这样一来,在这一要点上,Brave 成了明显的弱点,别看运用字节码注入何等艰难,可起码还是能够达成的,然而 Brave 存有无法着手的概率,并且能不能实施注入,能在多大程度上进行注入,更多是取决于框架的 API 而非自身具备的能力。

难度及成本

经由对 Pinpoint 和 Brave 插件的代码予以简单阅读,能够发觉两者在实现难度方面仿若有着天堑般大的差距。在全然不存在任何开发文档为其提供支撑的这样一种前提条件之下,Brave 相较于 Pinpoint 而言更易于上手。Brave 的代码数量颇为少,核心功能全都集中于 brave-core 这个模块当中,一位具备中等水平的开发人员,能够在一天的时间之内将其中的内容读懂,并且对于 API 的结构能够有着极为清晰的洞悉。

Pinpoint的代码封装相当不错,特别是针对字节码注入的上层API的封装极为出色,然而这依旧需要阅读人员对字节码注入具备一定了解,虽说其用于注入代码的核心API数量不多,可是要想透彻理解,恐怕还得深入Agent的相关代码,比如很难一眼就明白addInterceptor和addScopedInterceptor的区别,而这两个方法就处在Agent的相关类型之中。

虽说 Brave 的注入得依靠底层框架去供给相关接口,然而对于框架没必要有着全面的认知,只需晓得在何处能够进行注入,以及在注入之际可以获取什么样的数据就行。如同上面所举的例子似的,就算对于 MySQL 的 JDBC Driver 是怎样实现的我们全然不知,也照样能够达成拦截 SQL 的能力。然而,Pinpoint 并非如此,由于 Pinpoint 差不多能够于任何位置注入任何代码,这要求开发人员对所要注入的库的代码实现具备极为深入的知晓,借由查看其 MySQL 以及 Http Client 插件的实现便能洞悉这一点,当然,这也从另外一边表明 Pinpoint 的能力着实能够无比强大,并且其默认实现的诸多插件已然达成了极为细粒度的拦截。

当底层框架不存在公开 API 的情形下,实际上 Brave 并非全然束手无策,咱们能够运用 AOP 的途径,照样能够把相关拦截注入进指定的代码里,并且很明显 AOP 的运用相较于字节码注入可要简易许多。

以上这些,直接关联到达成一个监控的成本,在Pinpoint 的官方技术资料里,给出了一份参考数值,如果针对一个系统进行集成,那么耗用在研发Pinpoint插件的成本是100,把这个插件融入系统的成本是0;然而对于Brave,插件研发的成本仅有20,而集成成本是10。从这一点能够看得出官方给出的成本参考数值是5:1。可是官方再度做出强调,要是有十 来个操作系统需要进行集成的情形下,那么总的成本数目即为,十乘以十再加上二十,其结果等于一百二十,如此一来便超出了Pinpoint的开发成本一百,并且需要集成的服务数量越多,这般的差距就愈发明显。

通用性和扩展性

非常明显,在这一要点之上,Pinpoint全然处于不利的态势,由社区所研发推出的集成接口便能够明显地看出来。

查找精确位置的数据接口缺少文档,并且不太规范地(参照论坛讨论帖子当中所提及的内容),要去阅读大量代码才有可能达成一个属于自身的探针(像针对Node的或者针对PHP的)。而且团队出于性能方面的考量采用了Thrift当作数据传输协议标准,相较于HTTP以及JSON而言难度增添了许多。

社区支持

不用再多说这一点,Zipkin是由Twitter开发的,那可是明星团队,而Naver团队仅仅是个默默无名的小团队(从#1759的讨论里便能看出)。虽说这个项目近期不太可能消失或者停止更新,然而终究不像前者使用起来那般令 人安心。并且没有更多社区开发出的插件,致使Pinpoint仅靠团队自身力量去完成诸多框架的集成着实困难,而且他们当前工作重点依旧是在提升性能以及稳定性上。

其他

Pinpoint在进行实现起始阶段,就对性能方面的问题予以了考量,www.naver.com网站的后端部分服务,每日所要处理的请求数量是超过二百亿次的,所以,他们会去挑选Thrift的二进制变长编码格式,并且采用UDP当作传输链路,与此同时,在对常量进行传递时,也尽可能地运用数据参考字典,传递一个数字而非直接去传递字符串等。这些优化增添了系统的复杂度,其中涵盖使用 Thrift 接口的困难程度,存在 UDP 数据传输方面的状况,并且有数据常量字典的注册相关问题等等。

相比较而言,Zipkin运用为人熟知的Restful接口加上JSON,几乎不存在任何学习所需的成本以及集成时的难度,只要了解数据传输的结构,便能够轻而易举地为一个全新的框架研制出相应的接口。

另外,Pinpoint欠缺针对请求的采样能力,很明显,在大流量的生产环境当中,将全部的请求都记录下来不太有可能,这便需要对请求开展采样,以此来确定什么样的请求是自己需要记录的。Pinpoint以及Brave都对采样百分比予以支持,也就是百分之多少的请求会被记录下来。然而,除此以外,Brave还给出了Sampler接口,能够自定义采样策略,特别是在进行A/B测试之际,这样的功能极具意义。

总结

从短期目标这个角度来讲,Pinpoint的确有着那种压倒性的优势,具体表现为不用对项目代码做出任何改动就能去部署探针这事,追踪数据能够细粒化到那种方法调用级别里,具备功能强大的用户界面,还有几乎比较全面的Java框架予以支持。但是从长远的趋向出发考虑,学习Pinpoint的开发接口这一情况,以及未来针对不同框架而去实现接口时所涉及的成本,目前都还是处于未知数这样一种状态。相反的是,掌握Brave相对而言会显得容易一些,并且Zipkin的社区展现出更强大的态势,更加具备在未来开发出更多接口的可能性。处于最为糟糕的状况时,我们能够凭借AOP的途径,自行增添契合自身的监控代码,于此过程中无需引入过多全新的技术以及全新的概念。并且在未来业务发生变更之际,Pinpoint官方所提供的报表究竟能否满足需求,其实难以确定,增添新兴的报表还会引发难以预测的工作难度以及工作量。

5Tracing 和 Monitor 区别

Monitor能划分成系统监控以及应用监控,系统监控,类似关乎CPU,内存,网络,磁盘等诸多方面的系统负载数据,进一步细化的话也能够具体到各个进程的相关数据,这一种类别的信息是能够直接从系统里获取到的,应用监控需应用给予支持,从而暴露了对应的一些数据,像应用内部请求的QPS,请求处理的延时,请求处理的error数,消息队列的队列长度,崩溃情况,进程垃圾回收信息等,Monitor的主要目的是找出异常,及时进行报警。

Tracing 的基础是调用链,其核心也是调用链,相关的 metric 大多是围绕调用链分析而得到的,Tracing 的主要目标是系统分析,提前找到问题而非在出现问题后再去解决要更好。

Tracing与应用级的Monitor技术栈存在诸多共同点,均涵盖数据的采集、分析、存储以及展式,只不过具体实施阶段所收集的数据维度有所差异,且分析过程也不尽相同。

另外,有个这样的情况,有一位作者,他完成了两篇专栏文章,一篇是关于Mybatis进阶的,另一篇是Spring Boot进阶的,之后,他还把这专栏文章整理成了书,要是有需要的读者,发现在某个公众号上,只要回复关键词Mybatis进阶、Spring Boot进阶,就能免费获取。

往期推荐

文章有帮助的话,在看,转发吧。

谢谢支持哟 (*^__^*

  • 温馨提示:以上微服务架构下全链路监控组件的产生背景及面临的问题的资讯来自颍上人才网(颍上地区找工作,发布招聘信息的颍上人才网招聘网站),每天更新颍上最新招聘信息。本内容地址:http://www.ysjob.cc/article/articledetail-389341.html转载请注明
最新资讯
微信分享

关注微信公众号

访问手机版本