字节跳动服务网格基于 Hertz 框架的落地实践

原创
07/27 12:02
阅读数 2.6K

随着企业用户逐渐增多,面对不同场景下的需求和技术问题,CloudWeGo 团队将会持续分享不同企业的落地实践,包含不同行业面临的技术问题、选型参考、最终落地性能和使用分享,来帮助更多用户使用 CloudWeGo

字节服务网格承载着线上超大规模的微服务调用,作为集中式控制面,面临着高性能挑战。字节跳动服务网格通过使用 CloudWeGo 团队的高性能务 HTTP 框架 Hertz,完成了超复杂调用网络下的流量治理体系的落地实践。以下内容来自字节跳动服务网格研发工程师  兰新宇 的分享。

服务网格在字节跳动的沉淀与积累

服务网格有得天独厚的架构优势,使得它能够在各种情况下解决业务的痛点,包括架构的优雅性等等,很多互联网大厂陆续将自己的云原生架构从传统的单体或者分布式的应用迁移到云原生服务网格上,字节跳动也不例外,今年开始大体量地使用服务网格。那么什么是服务网格呢?

什么是服务网格

首先简要介绍一下服务网格的概念。如下图所示,服务网格并不是一个特别新鲜的架构,我们常说加一层代理就可以解决很多事情,服务网格从最简单的层面理解就是加一层代理,把流量通过代理来转发实现架构的剥离。这种处理方式会带来很多好处,下面重点介绍三个字节通过服务网格解决的痛点。

第一个是服务网格可以使业务进程和框架解耦。原来的进程是框架集成在一个进程里面,通过 SDK 的形式来提供,我们通过提供隔离的 Proxy,以一个分离的进程和业务进程独立存在,这是单一职责在 RPC 通信领域的体现,这样业务和 RPC 框架研发同学可以更专注于核心功能的实现。

第二个是灵活、可靠的生命周期管理。我们通过进程剥离的方式,使得我们不再依赖业务升级自己的服务来达到框架版本的升级,服务网格的开发、运维者能够对生命周期做更好的把控,这个是对服务网格的开发者或者运维者非常有利的特征。

第三个是统一的跨语言流量治理体验。我们把它通过中间层的方式实现单独代理,接管所有流量,我们可以针对所有不同语言实现的业务进程统一治理体验,在字节跳动,多语言技术栈非常普遍,C/C++/Python/Java/Golang 等服务都有很强的流量治理诉求,所以服务网格也可以针对这种情况给他们提供治理体验的提升。

典型架构

下面跟大家分享一个典型架构,以便后面引出字节内部的服务网格使用情况。社区内关注量比较高的服务网格架构,如 Istio 和 Envoy 架构,它们会把服务网格分为两个层面,一个是数据面,一个是控制面。如图所示,数据面以一个单独的进程或者代理模式与业务进程在同一个 POD 里面运行,起到分离的作用。数据面更关注是怎样做一个高性能的代理,除此之外,还关注流量治理能力的实现,如超时、垄断、降级等,把这些能力从 SDK 里面剥离出来,在数据面真正地实现跨语言治理体验的特性。

控制面是为所有网格的数据面提供完整的流量控制功能。不只是在流量里面的控制能力,还包括对于 POD 或者服务之间通信上有安全层面的要求,安全控制也可以通过网格数据面和控制面的通信达成。此外,当服务的体量较大或者系统较为复杂时,我们需要达到很高强度的可观测性,以度量系统的稳定性以及系统能够带来的其他收益,因此可观测性也是网格控制面一个非常重要的特性。

落地挑战

第一,字节内部有很多跨语言治理的诉求。除了最广泛使用的 Go 语言,还有 Python,Node,C++ 和 Java 等等,包括最近比较受欢迎的 Rust 语言。因此字节内部要落地服务网格实际上面临着比较大的挑战。

第二,字节内部有很多服务量级以及这些服务对应的容器。据估计,在线微服务数量超过 10 万,容器实例部署的量级大概是 1000 万。面对如此大的体量,服务网格的控制面应该如何保证高可用性、稳定性和资源的开销?控制面和数据面之间的通信协议应该如何设计?怎样让更多新的服务和新的容器快速地接入我们的网格以享受便捷服务?

第三,我们面临着非常复杂的业务形态。字节有很多用户端产品,比如抖音、今日头条和懂车帝,当然还有联盟和电商之类的产品。不同的业务形态对网格的诉求也是不一样的,比如抖音是一个强依赖“读”请求的产品,它需要快速地获取大量数据,电商是强依赖“写”请求的产品,这类产品会收到很多订单请求,网格要保证这些的请求写入成功率。因此对于字节这种复杂的业务形态,我们的网格面临着很强的挑战。

落地实践

如图所示,为了应对这些挑战,我们拓展了服务网格的通信功能,将其中的一个 POD 进行细化。可以看到图中最上面有一个业务的进程,其次有传统的服务网格数据面 Data Plane,也就是 Proxy,此外字节内部还会有一个专门的运维 Operation Agent。在此基础上,数据面会与控制面进行通信,同时我们内部也会有专门的运维来做服务网格生命周期的管理。那么字节跳动服务网格的数据面、控制面和运维面都具备什么特点呢?接下来进行具体介绍。

1. 数据面

首先数据面会有动态配置能力,比如我们可能需要配置缓存的过期时间、缓存的规模、缓存是否淘汰等等来降低海量请求给控制片带来的压力。其次部署了大量服务后,每一个服务都有一个数据面代理,那么我们就需要把性能做到足够高。对于这么多容器,哪怕代理层只节省一点资源,整体的线上资源都可以节省一大部分。所以我们考虑基于 Share Memory IPC 运行,这会使得网络通信量很大的容器尽可能使用本地内存通信,可以大大降低网络通信开销和反序列化开销。最后是在编译或者链接时做一些基于程序的优化,比如 LTO 等等。以上就是我们从数据面的角度考虑如何进行服务网格的优化。

2. 控制面

首先,我们采用纯自研的方案,没有使用 ACE,可以灵活支持服务动态接入网格。其次,我们也通过多集群部署将服务以不同的业务形态或者业务线做区分,然后有效地控制爆炸半径。同时在整个优化过程中,也可以以优先级的形式进行排位,从而逐步地上线功能。最后是控制面具有多样的流量治理能力,能够持续为业务赋能。面对不同种类的业务诉求,我们也在尝试不断地解决新出现的问题和挑战。

3. 运维面

首先,我们会做到全面掌控网格内部通信,也就是控制面和数据面的通信方式。其次,在服务网格数据面做升级或者变更时,我们要保证网格的进程能够正常地运行,因此发布确认机制是需要有非常强的保障。最后,对于不同的服务或者业务线,对代理的特殊配置需要进行版本的锁定,在这方面我们也有比较丰富的经验。

超复杂调用网络下的流量治理体系

基于字节跳动如此大体量的服务网格,我们给网格的用户带来了什么呢?这就要介绍一下在超复杂的调用网络下的流量治理体系。

核心能力

我们有非常多流量治理的功能,基于近几年的打磨,形成了一套相对比较完整的治理体系。下面重点分享三个核心能力。

第一,微服务访问控制。字节内部秉持着“零信任”的宗旨,我们认为不论是外网还是内网,微服务之间的访问都是要有约束的、是不可信任的,所以我们大规模落地了微服务之间的访问控制能力。访问控制包括以下几方面:

  • 授权,访问的权限;

  • 鉴定,对自己身份的介绍是否是可信;

  • 流量加密,防止嗅探或攻击流量,保证数据层面的安全。

第二,单元化。随着业务形态的复杂程度不断加深,会有各种各样的场景出现,因此需要很强的隔离机制。我们内部在单元化(流量管控/流量隔离)方面积累了很多经验。我们不仅仅支持传统的 HTTP、Thrift 和 RPC 等流量,还支持 Message Queue 这种类型的流量。

第三,动态治理。随着服务网格和机器量级逐渐扩大,我们面临的一个挑战是可能会有很动态的场景,不能通过非常简单或者一成不变的配置来实现,它更多需要的是一种动态过载控制和动态负载均衡的能力,在这方面我们也是有一定积累的。

落地规模

那么这些核心治理能力在字节内部的落地规模是怎样的呢?大概有超过 13,000 个服务都会使用授权能力,超过 2500 个服务开启了非常严格的身份认证能力。身份认证是基于字节内部一个相对比较成熟、稳定的身份颁发系统来进行。单元化的部分我们有超过 1000 条隔离链条做流量隔离。为了实时地保护自己的服务所对应容器的容量状态,有超过 7500 个服务使用我们动态过载控制的能力。有超过 2500 个服务接入了我们动态负载均衡能力,使得流量能够尽可能地基于实际云环境的负载情况做动态流量调动。以下是具体使用情况介绍。

微服务访问控制

结合下图具体介绍一下在字节内部授权、鉴定和流量加密三种能力的使用场景。如图上面是一个比较传统的服务网格架构图,我们加入了 POD A 和 POD B,它就是通过我们的网格做流量的传递和通信。下面的 POD B 是一个没有接入网格的服务,它只有自己的业务进程。中央的控制面分两个模块,这里面展示的是我们在介绍时会用到的两个模块,一个是观测方面的,一个是安全方面的。安全方面主要做下发配置以及身份相关的校验。

  • 授权

数据层是会做允许名单和不允许名单、拒绝或放行的操作,以及数据的解析和匹配等等。同时授权也提供了比较强的观测能力,因为对于如此大体量的服务,如果遇到没有得到授权的调用方来访问,贸然开启授权是有很大风险的,所以我们提供了相对比较成熟的观测能力。所有的服务都可以在类似 Dry Run 的场景下看有哪些调用方没有得到对应权限,然后进行相应的操作,这个就是由控制面的观测模块实现的。同时,业务同学可能有非常多的上游,因此维护授权列表也是很复杂的,所以我们也同时提供了巡检的系统,一方面可以帮助他们给自己存量的服务做配置,另一方面我们也会去帮助他们控制增量的风险。

  • 鉴定

鉴定和授权比较类似,也会提供观测能力。同时我们考虑到基础身份服务不可用的情况,也提供了动态灵活的降级能力,比如过期身份、无效身份或没有传递身份等等。

  • 流量加密

为了保障线上的大部分服务(不论是否应用了服务网格)能够尽可能平滑地做流量加密,我们在控制面做了极端场景的考虑,这会自动地帮助我们已经配置过的、开启服务网格的所有容器之间进行加密流量,也就是图中 POD A 到 POD B 的请求。如果下面的 POD 不具备流量加密或者解析的能力,我们会自动地给它发送非加密的请求,使它达到比较安全的接入状态。

单元化

在单元化方面服务网格面临的使用场景具体如下:

第一,由于传输进来的流量标识不同,用户可能想要根据不同流量标识对租户进行不同的处理,之后根据不同的集群或者服务做相应的策略。

第二,可能有一些服务具备常态化容灾演练的需求,比如有一些专门供应演练的集群。

第三,有很多敏感服务需要做流量的隔离,比如某些东西对某些人绝对不可见。这些都是比较典型的单元化场景。基于这些用户场景,我们采取了相对比较灵活的单元化流量调度。

单元化的特点有以下四个方面:

第一,请求特征级别的智能路由能力。实际上这是我们单元化的基础,基于请求特征决定流量的发送位置。

第二,请求特征级别的治理能力。在决定流量发送位置的基础上,还可以决定你是否有发送到对应位置的权限。

第三,动态更新,无业务入侵。那么超时或熔断降级等等这些参数应该怎么设置呢?这两种非常灵活的智能路由和治理能力使用条件也是非常简单的。首先,我们可以动态更新,只要接入我们的服务网格,就可以直接感知添加代码的参数或者使用哪种中间件。其次,可以采用观测能力。

第四,支持观测、按比例灰度上量。想知道流量是否可以正确地发到指定位置,我们可以先不用真正发送,而是观测一下它对应的情况是否符合预期。在确定没有问题之后,网格还具有按比例灰度上量的能力,直到你觉得完全没有问题,可以全部切过去。这极大地降低了用户对于单元化能力的担心,可以慢慢地把自己的流量做单元化,实现自己的诉求。

动态治理

1. 过载保护

过载保护,即这个服务保证自己不会过载、相对比较稳定后对外提供服务的能力。实际上我们基于 Mesh 也会做过载保护。我们参考了很多业界文章,比如微信、Google 等公司,借鉴了他们在过载保护方面的一些沉淀,以此来考虑我们是否有合适的参数或指标衡量一个服务或容器的过载情况。下图是对应的两个不同容器。首先 POD A 是一个 API 层的服务,它在通过 LB 层的流量进来后,会自动地根据一些请求特征注入请求优先级的数据。

什么是请求特征?这个优先级是什么样子的?具体来讲,如果请求是从某一个 HTTP  Server 的 URL 进来的,那么它会具有这样的特征。比如一类请求是抖音的 Feed,它的请求优先级是 A,另一类请求是抖音的评论,从另一个 URL 进来,它的请求优先级是 B,对应 A 和 B 请求优先级在业务形态上的重要性是不一样的。我们在后面做扩展的时候会使用优先级作为一个很重要的评判指标。

在 A 和 B 里面都有一个流程,在收到请求之后,会有一个接收到请求的时间点,在这个时间点的基础上,我们会把这个请求转化到业务进程。业务进程在使用这个请求或者在解析这个请求的时候,会有一段处理时间,它也会把这个处理时间做标记,放在请求回复里面。之后我们在数据面代理会捕获到“请求回复的时间戳”,与我们标记的“请求到达的时间戳”做相应的计算,我们把这个指标定义为排队时间。

通过动态地更新排队时间的状态机,我们可以评判业务的情况以及它是否过载。相应地我们也会将排队时间延迟成功的请求量、错误量等等,通过异步上报的形式传达到控制面的观测模块,基于 CPU 利用率、排队时间、单位时间内的请求量、客户端超时断点等等这些输入,我们可以通过状态积载流转出。

那么这个服务是否处于过载状态?如果它处于过载状态,我们会得到一个输出,即当你的请求优先级小于某一个值的时候,我们会将这个请求丢弃掉。这也是相对比较动态地基于服务运行时的指标评判容器过载情况,它可以快速地使服务从过载的状态恢复,同时也可以缓慢地使服务的过载状态达到可应用状态,这会使得这个服务在压力比较大的情况下,仍然能够为一些高优先级的请求提供服务,保证客户层面看到的可用性。

2. 负载均衡

字节内部的内网环境非常复杂,信息数量比较大,我们经常会碰到一些坏的机器,比如 CPU 主频比较低等。在这种情况下部署的服务处理能力可能没有其他的机器强,所以偶尔会反映出 CPU 水平不均,或者单实例的问题。

在面临这样的问题时,我们解决方案是什么呢?首先我们会从数据面和控制面着手。数据面更关注的事情是高精度指标的采集和上报,它的精度可能会高于绝大部分观测系统的精度,这使得我们的系统能够更快速地反映当前状态。然后这种指标被上报到控制面做聚合,进行处理分析。同时,数据面只需要支持非常简单的基于权重的负载均衡算法即可,后续我们会用这个权重做动态治理流量的调节。

控制面可以做基于指标的动态权重计算,比如我们有 RPC 的成功请求量、错误量和一些其他的错误改作时间等等,那么我们可以基于这些指标做聚合,即拿到所有从调研方视角来看被调用方的容器信息负载情况、延迟情况做中心化的计算。计算输出会产出一个实例,即它的动态权重应该是多少,也就是它实际上应该承载请求的比例。然后把对应的服务发现结果下发给数据面,数据面需要采用非常灵活的权重变化实现动态负载均衡,这就可以解决我们前面所面临的问题。也就是说尽可能规避那些出现故障的机器,将尽可能少的流量发到处理能力较差的容器。

Hertz 在服务网格的落地实践

技术选型

在我们的服务网格做技术选型的时候,我们面临着很多挑战和困难。我们比较关注的技术选型特点有三个方面。

  • 性能

我们存量的流量比较大,服务的量级也比较多,因此想要在框架方面尽可能节省。当时调研了一些备选框架,当然那时 Hertz 还没有开源。我们的面临问题是根据 13M 以上单位请求下的流量做框架的选型。

  • 易用性

实际上我们做服务网格的设计时,也会经常考虑到网格对于用户的易用性。在我们选择框架时,我们就变成了框架的用户,因此我们也会考虑框架的易用性。我们比较关心框架对两个特点,第一,我们是否可以很容易地通过框架写出高质量代码,而不用关心过多的细节。第二,框架本身是否提供了比较易用且丰富的 API 供我们直接使用,使得我们尽可能少地造轮子。

  • 长效支持

在选择框架的时候,我们会考虑要用已经开源的、还是用内部资源或者自己重写一个框架?如果用已经开源的框架,遇到了问题怎么办?此外,我们可能要二次开发加一些新的功能,面对这些 Features 社区是否有足够的人力进行支持?这些也是我们选择框架的时候面临的问题。

Hertz 介绍

最终我们选择了 Hertz 框架,也就是最新已经开源的 CloudWeGo/Hertz。它实际上是字节跳动服务框架团队开发并且开源出来的一个基于 Golang 的高性能 HTTP 框架。我们使用后体验到 Hertz 框架有以下三个方面特点

1. 高易用性。一个能够在字节内部沉淀两年没有被淘汰,而且还有很多业务在使用的框架,它的易用性肯定是非常高的。

2. 高性能。高性能实际上是我们做框架选择一个很重要的考察点。在开源方案里面为数不多的高性能框架,如  fasthttp,它实际上也是我们为数不多的选择之一。如果一个框架兼备高易用性和高性能,是很难得的。

3. 高拓展性。从目前的使用情况来看,Hertz 的拓展性还是很强的。因为我们服务网格需要框架具有高拓展性以满足我们的一些特征,包括多协议的支持等等。大家可以从 Hertz 官网了解更多的相关细节。

Hertz 官网:cloudwego/hertz: A high-performance and strong-extensibility Go HTTP framework that helps developers build microservices. (github.com)

Telemetry Mixer

Hertz 在服务网格内部是怎样使用的呢?首先就是刚刚提到的动态负载均衡的观测模块,这个模块的服务类型是一个 HTTP 服务,也就是我们数据面代理的指标通过 HTTP 服务做中转,以便于我们在整个控制面的架构核心里面做后续数据处理。如下图,Mixer 部分是使用 Hertz Server 实现的。我们充分利用它高并发、高吞吐的特点,以及为了协议尽可能达到高易用性和可解释性,我们使用了 JSON。当然也是因为考虑到 Hertz 可以无缝集成基于 Sonic 的高性能 JSON 编解码方案。

我们从 HTTP 层或 API 层接收到数据后,使用 CloudWeGo 社区的另一个开源框架 Kitex,从而使内部系统通过 Thrift 高效编解码的方式做数据流转,以达到减少开销的目的。同时因为我们对数据有比较高的自定义流量调度能力,所以也使用了 Kitex 拓展性比较强的功能,即自定义负载均衡策略,将相应的指标从 API 层转到自研数据库做比较复杂的指标聚合,再全程计算。我们也会做指标异步的存储 InfluxDB 等等用于后面的观测性看板。

此外,我们也使用了 Kitex 在 Thrift 和 Frugal 方面的相关调研,以尽可能地降低协议、反序列化相关的 CPU 内存等等资源的开销。关于这个方面,我们也还在与 Kitex 团队同学合作进行调研和测试。

Configuration Center

同时我们也使用 Hertz 作为核心控制面配置中心 API 层的服务。这里的使用方法相对简单,仍然是使用 Hertz 为非接入 Mesh 的服务提供治理配置的拉取。

如下图所示,假设 POD A 接入了服务网格,使用数据面做数据存量的流量调度和处理,POD B 使用的是 Kitex 框架,与业务进程同时在一个容器里面提供服务。对于这样服务网格和非服务网格的服务之间的调用,数据面会向控制面的 Service Mesh Core 请求对应的配置和服务发现结果,框架会向控制面的 Configuration Center 请求对应的服务治理相关配置。实际上配置中心的请求量级与容器的数量是相关的,同时接入服务网格的部分服务框架,我们会请求配置中心获取一些必不可少的配置信息。

收益分析

使用了 Hertz 之后,给服务网格带来的收益大概是怎样的呢?这可以从三个方面进行分析。

第一是从性能方面,最初选择 Hertz 也是与很多开源框架从性能方面进行了对比,Hertz 是内部产品,当时还没有开源,但是它的性能做得非常好。我们具体从性能方面考虑了两个主要的点,一个是这个框架可以稳定地承载线上超过 13M QPS 的流量,另一个是在上线前后和我们测试过程中发现 CPU 火焰图的占比是符合预期的。这一点后续我们会根据性能再详细地展开介绍。

第二是从应用性方面,这实际上也是我们非常关注的点,即我们如何基于这样的一款框架快速地实现所需要的功能,从而尽可能关注我们的业务逻辑,而不是框架本身。还有我们如何基于这个框架实现一些高效代码,避免还要花费更多精力学习相关语言。

第三是从长效支持方面,目前 Hertz 已贡献给 CloudWeGo 开源社区,而且开源和内部为统一版本,这也为给我们提供长效支持提供了保证。

下面详细地从这三个方面进行具体介绍。

收益分析之性能

因为字节内部业务容器数量过多,因此我们服务的 Goroutine 数量比较多,从而导致稳定性较差。

通过 CPU 火焰图,可以看到从 Gin 替换成 Hertz 之后,我们获取的收益主要有四点。

第一,同等吞吐量下具备更高稳定性。Hertz 的设计和实践都是基于 Netpoll 网络库实现的,在同样的吞吐量下,它就会比其他框架具备更高的稳定性。

第二,Goroutine 数量从 6w 降到 80。之前 Goroutine 的数量是 6 万,使用 Hertz 从 6 万直接降到不足 100 个,Goroutine 稳定性得到极大地提升。

第三,框架开销大幅降低。从火焰图可以看到,替换成 Hertz 后,之前 Gin 框架相关的开销已经基本消失不见。更多的还是像网络序列化、反序列化和业务逻辑相关的开销。

第四,稳定承载线上超 13M QPS 流量。替换成 Hertz 后,服务网格在线上稳定承载了超过 13M QPS 的流量。

最后再介绍一下替换前后 CPU 优化情况,替换成为 Hertz 框架后,CPU 流量从大概快到 4k 降到大约只有 2.5k。

收益分析之易用性

我们之前面临的痛点问题是很难基于已有的框架写出非常高性能的代码,可能需要对 Go 语言有更深入的了解。同时我们很难设计一个好的容错机制,比如要基于已有的框架去实现一个高性能的代码,我们要关注连接池、对象池,然后去控制连接超时、请求超时和读写超时等等。这中间如果有一个环节实现错误,就会导致非常灾难性的后果,之后我们要不停地回滚上线修复。

使用 Hertz 之后,代码实现变得非常简单。Hert自带比较易用的对象回调机制,我们不用再特别关注请求和响应的对象复用,以及怎样配置连接超时和请求重试,这些都是 Hertz 框架直接提供的,用户只需按照它的说明进行配置,它就可以达到用户预期。

收益分析之长效支持

与性能和应用性相比,我觉得长效支持是更加重要的特性。我们之前在选择框架时也考虑了一些其他开源项目,但它们的迭代速度往往达不到预期。比如 Fasthttp,它是一个非常优秀的开源高性能 HTTP 框架,但如果用户有 HTTP/2.0 或者 WebSockets 相关的需求,会发现最近几年它都没有做相应的支持,在官方文档上也声明这方面的研发还在进展之中。

所以在选择框架或开源方案时,我们可能会遇到一些这样的问题,如果我们真的要使用它并且要达到预期,可能就要被迫维护开源项目的克隆。如果过一段时间开源项目迭代了,我们还要花时间做 Rebase 或与社区的同步等等,这个维护过程成本比较高。

那使用 Hertz 的收益是什么呢?首先就是并不是所有的开源项目都有很强大的社区持续运营。CloudWeGo/Hertz 是由字节内部非常强大的社区组织运营的,同时 Hertz 也是字节跳动内部广泛使用的框架。业务同学或其他公司用户使用 Hertz 时遇到的问题在字节内部使用过程中都已经出现过,而且已经针对相关问题做了修复或者功能的添加,因此用户的要求大部分需求会直接得到满足。

同时 CloudWeGo/Hertz 字节内部的使用版本是基于 Hertz 的开源版本构建的。很多特性在内部上线之后,我们也会同步地发布到外部的开源版本上,使得大家能够快速地使用到这样的特性,享受到 Hertz 更高的性能。

CloudWeGo 社区已经发布的 Kitex、Netpoll 和 Hertz 等项目,都在持续进行迭代。最近 Hertz v0.2.0 也已经正式发布,欢迎大家到 CloudWeGo 官网查看相关信息。

另外,【CSG 第二期】CloudWeGo 源码解读活动 ——“Hertz 框架篇”开始啦!活动链接:【CSG 第二期】转发海报送周边好礼,CloudWeGo 源码解读活动 ——“Hertz 框架篇”开始啦! (qq.com)

 更多资讯

展开阅读全文
加载中

作者的其它热门文章

打赏
0
2 收藏
分享
打赏
0 评论
2 收藏
0
分享
返回顶部
顶部