程序锅

  • 首页
  • 分类
  • 标签
  • 归档
  • 关于

  • 搜索
基础知识 Etcd LeetCode 计算机体系结构 Kubernetes Containerd Docker 容器 云原生 Serverless 项目开发维护 ELF 深入理解程序 Tmux Vim Linux Kernel Linux numpy matplotlib 机器学习 MQTT 网络基础 Thrift RPC OS 操作系统 Clang 研途 数据结构和算法 Java 编程语言 Golang Python 个人网站搭建 Nginx 计算机通用技术 Git

Serverless | Serverless 概念和 FaaS 讲解

发表于 2020-11-14 | 分类于 Serverless | 0 | 阅读次数 3293

1. Serverfull 到 Serverless 的演变

上图是 MVC 架构的 Web 应用部署之后的典型情况。上图中的整个蓝色部分就是服务端的边界,它是负责应用或代码的线上运维。而 Serverless 要解决的问题的边界就是服务端的边界,也就是服务端运维。

那么下面我们先来看一下服务端运维的发展史,也就是从一开始到 Serverless 的发展史。假设有一个 Web 应用,这个 Web 应用的研发涉及到两个角色:研发工程师和运维工程师。研发工程师只关心应用的业务逻辑。具体来说就是,整个 MVC 架构 Web 应用的开发都归他负责,也就是从服务端界面 View 层,到业务逻辑 Control 层,再到数据存储 Model 层,整个 Web 应用的版本管理和线上 bug 修复都归研发工程师。运维工程师则只关心应用的服务端运维事务。他负责部署上线小程的 Web 应用,绑定域名以及日志监控。在用户访问量大的时候,他要给这个应用扩容;在用户访问量小的时候,他要给这个应用缩容;在服务器挂了的时候,他还要重启或者换一台服务器。

  • Serverfull 时代。最开始的时候,研发工程师不用关心任何部署相关的事情。研发工程师每次发布新的应用后,运维工程师都负责部署上线最新的代码。运维工程师需要管理好迭代版本的发布,分支合并,将应用上线,遇到问题回滚。如果线上出了故障,还需要抓取日志发给研发工程师。

    Serverfull 时代将研发和运维完全隔离开来了。这种完全隔离开来的好处很明显:研发工程可以专心做好自己的业务,但是运维工程师就成了工具人了,就困在大量的运维工作中,处理大量琐碎的杂事。

  • DevOps 时代。运维工程师发现有很多事情都是重复性的工作,线上出故障了还得自己抓日志发给研发工程师,效率很低。因此运维工程师就开发了一套运维控制台,将部署上线和日志抓取的工作让研发工程师处理。

    这样,运维工程师可以稍微轻松点了,但是优化架构和扩缩容资源方案还是得负责。而研发工程师除了开发的任务,还要自己通过运维控制台发布新版本和解决线上故障。这个时候是研发兼运维 DevOps,研发工程师兼任了部分运维工程师的工作,但是这部分的工作就应该是研发工程负责的(比如版本控制、线上故障等),而且运维工程师将这部分工作工具化了,更加高效了,有 less 的趋势了。

  • 工业时代。运维工程师又基于研发工程师的开发流程,将运维控制台进一步提升,可以实现代码自动发布:代码扫描-测试-灰度验证-上线。这样一来,研发工程师只需要将最新的代码合并到 Git 仓库指定的 develop 分支,剩下的就由代码自动发布的流水线来负责了。这个时候研发工程师也不需要运维了,免运维 NoOps,研发工程师也就回到了当初,只需要关心自己的应用业务就可以了。

    同时,运维工程师发现资源优化和扩缩容方案也可以利用性能监控+流量估算解决。这样运维工程师的运维工作也全都自动化了。那么对于研发工程师来说,运维工程师的存在感越来越弱,需要运维工程师干的事情越来越少,都由自动化工具替代了。这就是 Serverless。

  • 未来。实现了免运维之后,运维工程师要转型去做更底层的服务,做基础架构的建设,提供更加智能、更加节省资源、更加周到的服务。而研发工程师可以完全不被运维的事情困扰,专注做好自己的业务,提升用户体验,思考业务价值。

    免运维 NoOps 并不是说服务端运维就不存在了,而是通过全知全能的服务,覆盖研发部署需要的所有需求,让研发工程师对它的感知越来越少。另外,NoOps 是理想状态,因为我们只能无限逼近 NoOps,所以说是 less,而不是 ServerZero。

Serverless 的 Server 限定了 Serverless 解决问题的边界,即服务端运维;less 说明了 Serverless 解决问题的目的,即免运维 NoOps。所以,Serverless 应该叫做服务端免运维,这也就是 Serverless 要解决的问题。

2. 什么是 Serverless

Serverless 要解决的就是将运维工程师的工作彻底透明化;而研发工程师只关心业务逻辑,不用关心部署运维和上线的各种问题。而要实现这种状态,那么就意味要对整个互联网服务端的运维工作进行极端抽象。而越抽象的东西,由于蕴含的信息量越大,所以越难定义。

但是,总的来说 Serverless 的含义有这两种:

  • 狭义 Serverless(最常见)是指 Serverless computing 架构 = FaaS 架构 = Trigger(事件驱动)+FaaS(Function as a Service,函数即服务)+BaaS(Backend as a Service,后端即服务,持久化或第三方服务)=FaaS + BaaS。
  • 广义 Serverless 是指服务端免运维,也就是具有 Serverless 特性的云服务。

2.1. 狭义的 Serverless

我们日常工作提到的 Serverless 都是指狭义的 Serverless。

而这主要是因为历史原因,2014 年 11 月份,亚马逊推出了真正意义上的第一款 Serverless FaaS 服务:Lambda。从此,Serverless 的概念才进入大多数人的视野,因此 Serverless 曾经一度就等于 FaaS。FaaS,函数即服务,它还有个名称叫作 Serverless Computing,它可以让我们随时随地创建、使用、销毁一个函数。

通常函数的使用过程:需要先从代码加载到内存,也就是实例化,然后被其他函数调用时执行。FaaS 中也是一样的,函数也需要实例化,然后被触发器 Trigger 调用。这两个最大的区别就是在 Runtime,也就是函数的上下文。FaaS 的 Runtime 是预先设置好的,都是云服务商提供的,我们可以使用但是无法控制。并且 FaaS 的 Runtime 是临时的,当 FaaS 的函数调用完之后,云服务商就会销毁这个实力,回收资源,也就意味着这个临时的 Runtime 会和函数一起销毁。因此,FaaS 推荐无状态的函数,也就是一个函数只要参数固定,那么返回的结果也必须是固定的。

那么将一开始的 MVC 架构的 Web 应用变成 Serverless 的话,那应该是怎样的呢?View 层是客户端展示的内容,通常并不需要函数算力;Control 层,就是函数的典型使用场景。在 MVC 架构中,一个 HTTP 的数据请求往往对应着一个 Control 函数,因此这个 Control 函数完全可以被 FaaS 函数代替。在 HTTP 的数据请求量大的时候,FaaS 函数会自动扩容多实例同时运行;在 HTTP 的数据请求量小的时候,又会自动缩容;当没有 HTTP 请求的时候,还会缩容至 0 实例。如下图所示:

Control 函数变成了无状态的,并且函数的实例在不停地扩容缩容,那么此时想要持久化一些数据怎么办?当然 Control 函数中还是可以以操作数据库的命令方式来实现。但是,这种方式并不合理,因为 Control 层的方式变了,假如 Model 层还是以之前的那种方式,那么这种架构肯定是要散架。此时,就需要 BaaS 了,也就是将 Model 层进行 BaaS 化,BaaS 就是专门配合 FaaS 用的。下面 Model 层以 MySQL 为例,Model 层最好将操作数据库的命令封装成 HTTP 的 OpenAPI,提供给 FaaS 调用,自己控制这个 API 的请求频率以及限流降低等。这个 Model 层本身则可以通过连接池、MySQL 集群等方式去优化。如下图所示:

至此,基于 Serverless 架构,传统的 MVC 架构完完全全被转化为了 View + FaaS + BaaS 的组合了。Serverless 毋庸置疑是因为 FaaS 架构才流行起来的。我们常见的 Serverless 都是指 Serverless Computing 架构,也就是由 Trigger、FaaS、BaaS 架构组成的应用。

2.2. 广义的 Serverless

广义的 Serverless 其实就是指服务端免运维,也是未来的趋势。要想达到 NoOps,需要具备:

  • 无需用户关心服务端的事情(容错、容灾、安全验证、自动扩缩容、日志调试)
  • 按使用量(调用次数、时长等)付费,低费用和高性能并行,大多数场景下节省开支。
  • 快速迭代&试错能力(多版本控制、灰度、CI&CD 等等)。

3. 为什么需要 Serverless 呢

在 2009 年的时候,有两种相互竞争的云虚拟化方法:

  • Amazon EC2,EC2 实例看起来很像物理硬件,用户可以从内核向上控制整个软件栈。
  • Google App Engine,是另一个针对特定领域的应用平台,它是一种将 stateless 计算层和 stateful 的存储层分类开来的一种应用程序结构。

最终市场使用了 Amazon 这种针对云计算的 low-level 虚拟机方式,而这种 low-level 虚拟机成功的主要原因是,早起的云计算用户希望在云中可以重新创建一个与本地计算机上相同的计算环境,以简化将其负载迁移到云上的工作。很明显,这种实际需求比为云重新编写新的程序更重要,尤其是在当时云计算能否成功尚不明确的情况下。

然后这种方式的缺点是,开发人员必须自己管理虚拟机,所以要么成为系统管理员,要么与它们一起设置环境。这些促使那些只使用简单化应用的客户向云服务商提出新要求,希望能有更简单的方式来运行这些简单应用。例如,假设应用希望将图片从手机端应用发送到云上,这需要创建极小的图片并将其放在 web 上,完成这个任务可能只需要几十行 JavaScript 代码,这与设置适当的服务器环境来运行这段代码相比,这个代码的开发是很微不足道的。

在这些需求的驱使下,Amazon 在 2015 年推出了一个名为 AWS Lambda service 的新服务。用户只需要编写代码,服务器供应和任务管理问题都由服务提供商来负责。编写的代码被打包为 FaaS(Function as a service),代表了 Serverless 计算的核心,但是云平台还提供了专门的 Serverless 框架,以满足特定的程序需求,如 BaaS(Backend as a Service)。简单地说,无服务计算定义如下:Serverless Computing = FaaS + BaaS。同时,服务被视为无服务的话,那么必须能够自动扩缩容,并且根据实际使用情况计费。

Cloud functions (i.e., FaaS) provide general compute and are complemented by an ecosystem of specialized Backend as a Service (BaaS) offfferings such as object storage, databases, or messaging.

4. Serverless VS Serverful

在 Serverless 中,用户只需要使用高级语言编写云函数,选择触发云函数运行的事件就可以了。例如,加载一个图像到云存储中,或者向数据库添加一个很小的图片时,用户只需要编写相应的代码,而剩下的全都由 Serverless 系统来处理,比如选择实例、扩缩容、部署、容错、监控、日志、安全补丁等等。下面,总结了 Serverless 和传统方式的差异,我们将传统方式称为 Serverful。

Serverless 和 Serverful 计算最关键的三个不同之处在于:

  1. **将计算与存储解耦。**存储和计算资源是分开提供的,相当于这两种资源的分配和计价都是独立的,通常来说存储资源是由一个独立的云服务来提供的,并且计算是无状态的。
  2. **执行代码而不需要管理资源分配。**与传统云计算用户需要请求资源的方式不同,serverless 是用户提交一段代码,云会自动给这段代码分配资源并执行。
  3. **以实际使用的资源量付费,而不是根据分配的资源数。**serverless 计费是根据一系列与执行相关的因素来计算的,例如代码的执行时间,而不实根据云平台,例如分配的 VM 的大小和数量

假如使用汇编语言和高级语言来形容的话,Serverful 计算类似于使用低级汇编语言进行编程,而 Serverless 计算类似于使用高级语言(例如 python)进行编程。例如,c = a + b 的简单表达式的汇编程序员必须显示选择一个或者多个寄存器,将值加载到这些寄存器中,执行运算,然后存储结果。这跟 Serverful 云编程的几个步骤是类似的:首先提供资源或者标识可用的资源,然后用必要的代码和数据加载这些资源,执行计算,返回或者存储结果,最终管理资源释放。而 Serverless 则提供了类似于高级编程语言的便捷性,Serverless 和高级编程语言也很相似性。比如,高级语言的自动内存管理不用再管理内存资源,而 Serverless 计算使程序员也不用再管理服务器资源。

5. Attractiveness of Serverless Computing

5.1. 对云服务提供商来说

  • Serverless 可以促进业务的增长,因为它使得云计算更容易编程,进而有助于吸引新客户并帮助现有客户更多地使用云计算。例如,最近的调查发现,大约 24% 的 Serverless 计算用户是云计算的新用户,30% 现有的 serverful 用户也使用了 Serverless 计算。
  • 短的运行时间、较小的内存占用和无状态特性使得云提供商更容易找到那哪些未使用的资源来运行这些任务,从而改进了资源复用。
  • 可以利用不太流行的计算机(实例类型由云提供商决定),比如对 serverful 云客户吸引较小的旧服务器。

后面的两点可以最大化现有的资源并提高收益。

5.2. 对用户来说

  • 从编程效率的提高中获益,对于新手来说不需要理解云基础设施的前提下部署函数,老用户可以节省出部署的时间并聚焦于应用本身的问题。
  • 节约成本,因为云服务提供商将底层服务器的利用率提高了,并且函数只有在事件发生时才会计费,而且是细粒度的计费(通常是 100 毫秒),那么也就意味着只需要支付他们实际使用的部分而不是为他们预留的部分。

6. FaaS 是怎么运行的

在 Serverless 出现之前,我们要部署这样一个"Hello World"应用得何等繁琐。

  1. 我们要购买虚拟机服务;
  2. 初始化虚拟机运行环境,安装我们需要的应用运行环境,尽量和本地开发环境保持一致;
  3. 紧接着为了让用户能够访问我们刚刚启动的应用,我们需要购买域名,用虚拟机 IP 注册域名,配置 Nginx,启动 Nginx;
  4. 最后我们还需要上传应用代码;
  5. 启动应用;

采用 Serverless 之后,只需要简单的 3 步。Serverless 相当于对服务端运维体系进行了极端的抽象(抽象意味着用户请求 HTTP 数据请求的全链路,并没有质的改变,只是将全链路的模型简化了)。

  1. 之前在服务端构建代码的运行环境---函数服务
  2. 之前需要负载均衡和反向代理--- HTTP 函数触发器
  3. 上传代码和启动应用---函数代码

整个启动过程如下图所示:

  1. 用户第一次访问 HTTP 函数触发器时,函数触发器会 Hold 住用户的 HTTP 请求,并产生一个HTTP Request 事件通知函数服务;
  2. 函数服务检查有没有闲置的函数实例,如果没有函数实例,则去函数代码仓库拉取你的代码,初始化并启动一个函数实例;之后再传入 HTTP Request 对象作为函数的参数,执行函数。
  3. 函数执行的结果 HTTP Response 返回函数触发器,函数触发器再将结果返回给等待的用户客户端。

FaaS 和 PaaS 平台对比,最大的区别在于资源利用率。这也是 FaaS 最大的创新点,FaaS 的应用实例可以缩容到 0,而 PaaS 平台至少要维持一台服务或容器。这主要是因为 FaaS 可以做到极速启动函数实例,而 PaaS 创建实例通常需要几十秒,为了保证你的服务可用性,必须一直维持至少一台服务器运行你的应用实例。

7. FaaS 的极速启动

FaaS 中的冷启动是指从调用函数开始到函数实例准备完成的整个过程。冷启动关注的是启动时间,启动时间越短,对资源的利用率就越高。现在的云服务商,基于不同的语言特性,冷启动平均耗时基本在 100~700 毫秒之间。

下面这张图是 FaaS 应用冷启动的过程。其中,蓝色部分是云服务商负责的,红色部分是用户负责的。云服务商会不停地优化自己负责的部分,毕竟启动速度越快对资源的利用率就越高,例如冷启动过程中耗时较长的是下载函数代码。所以一旦你更新代码,云服务商就会偷偷开始调度资源,下载你的代码构建函数实例的镜像。请求第一次访问时,云服务商就可以利用构建好的缓存镜像,直接跳过冷启动的下载函数代码步骤,从镜像启动容器,这个也叫预热冷启动。除此之外,还有预留实例策略也可加速或绕过冷启动时间。

FaaS 服务从 0 开始,启动并执行完一个函数,只需要 100 毫秒。这也是为什么 FaaS 敢缩容到 0 的主要原因。通常我们打开一个网页有个关键指标,响应时间在 1 秒以内,都算优秀。这么一对比,100 毫秒的启动时间,对于网页的秒开率影响真的极小。

为什么应用托管平台 PaaS 做不到极速启动呢?因为应用托管平台 PaaS 为了适应用户的多样性,必须支持多语言兼容,还要提供传统后台服务,例如 MySQL、Redis。这也就意味着,PaaS 在初始化环境时,有大量依赖和多语言版本需要兼容,而且兼容多种用户的应用代码往往也会增加应用构建过程的时间。

而 FaaS 设计之初就牺牲了用户的可控性和应用场景,来简化代码模型,并且分层结构进一步提升了资源的利用率。

8. FaaS 的分层

FaaS 实例执行时,就如下图所示,至少是 3 层结构:容器、运行时 runtime、具体的函数代码。

  • 目前的 FaaS 实现方案中,容器方案可能是 Docker 容器、VM 虚拟机,甚至 Sandbox 沙盒环境。
  • 运行时 Runtime,就是你的函数执行时的上下文 context。Runtime 的信息包括代码运行的语言和版本,例如 Node.js v10,Python3.6;可调用对象,例如 aliyun SDK;系统信息,例如环境变量等等。

这样分层的好处就是,容器层适用性更广,云服务商可以预热大量的容器实例,将物理服务器的计算碎片化。Runtime 的实例适用性较低,可以少量预热。容器和 Runtime 固定后,下载你的代码就可以执行了。通过分层,我们就可以做到资源统筹优化,让你的代码快速低成本地被执行。

另外,一旦容器 & Runtime 启动后,就会维持一段时间,这段时间内的这个函数实例就可以直接处理用户数据请求。当一段时间内没有用户请求事件发生(各个云服务商维持实例的时间和策略不同),则会销毁这个函数实例。

9. FaaS 进程模型

从运行函数实例的进程角度来看,有两种模型:

  • 用完即毁型:函数实例准备好后,执行完函数就直接结束。FaaS 最纯正的用法。
  • 常驻进程型:函数实例准备好后,执行完函数不结束,而是返回继续等待下一次函数被调用。即使 FaaS 是常驻进程型,如果一段时间没有事件触发,函数实例还是会被云服务商销毁。

从下面这张图其实可以看到触发器就是一个常驻进程模型,只不过这个触发器由云服务商处理罢了。

假设我们要部署的是一个 MVC 架构的 Web 服务,那么:

  • 在之前,假设没有 FaaS,我们要将应用部署到托管平台 PaaS 上;启动 Web 服务时,主进程初始化连接 MongoDB,初始化完成后,持续监听服务器的 80 端口,直到监听端口的句柄关闭或主进程接收到终止信号;当 80 端口和客户端建立完 TCP 链接,有 HTTP 请求过来,服务器就会将请求转发给 Web 服务的主进程,这时主进程会创建一个子进程来处理这个请求。

  • 而在 FaaS 常驻进程型模式下,首先我们要改造一下代码,Node.js 的 Server 对象采用 FaaS Runtime 提供的 Server 对象;然后我们把监听端口改为监听 HTTP 事件;启动 Web 服务时,主进程初始化连接 MongoDB,初始化完成后,持续监听 HTTP 事件,直到被云服务商控制的父进程关闭回收。

    当 HTTP 事件发生时,我们的 Web 服务主进程跟之前一样,创建一个子进程来处理这个请求事件。主进程就如我们上图中绘制的那个蓝色的圆点,当 HTTP 事件发生时,它创建的子进程就是蓝色弧形箭头,当子进程处理完后就会被主进程回收。

通过上面的例子,可以看到:常驻进程型就是为了传统 MVC 架构部署上 FaaS 专门设计的(显得很不自然,FaaS 原生的还是用完即毁型)。当然也可以使用用完即毁型来部署 MVC 架构的 Web 服务,但是不推荐这么做,因为用完即毁型对传统 MVC 改造的成本太大。

从可控性和改造成本角度来看 Web 服务,服务端部署方案最适合的还是托管平台 PaaS 或者自己搭服务跑在 IaaS 上。正如我上一讲所说,使用 FaaS 就必须在 FaaS 的条件限制内使用,最佳的做法应该是一开始就选用 FaaS 开发。

用完即毁型适用的场景:数据编排和服务编排。

  • 数据编排

    目前最成功最广泛的设计模式就是 MVC 模式。但随着前端 MVVM 框架越来越火,前端 View 层逐渐前置,发展成 SPA 单页应用;后端 Control 和 Model 层逐渐下沉,发展成面向服务编程的后端应用。这种情况下,前后端更加彻底地解耦了,前端开发可以依赖 Mock 数据接口完全脱离后端限制,而后端的同学则可以面向数据接口开发,但这也产生了高网络 I/O 的数据网关层。

    Node.js 的异步非阻塞和 JavaScript 天然亲近前端工程师的特性,自然地接过数据网关层。因此诞生了 Node.js 的 BFF 层 (Backend For Frontend),BFF 层充当了中间胶水层的角色,粘合前后端。将后端数据和后端接口编排,适配成前端需要的数据结构,提供给前端使用。

    未经加工的数据,我们称为元数据 Raw Data,对于普通用户来说元数据几乎不可读。所以我们需要将有用的数据组合起来,并且加工数据,让数据具备价值。对于数据的组合和加工,我们称之为数据编排。

    BFF 层通常是由善于处理高网络 I/O 的 Node.js 应用负责。传统的服务端运维 Node.js 应用还是比较重的,需要我们购买虚拟机,或者使用应用托管 PaaS 平台。但是,由于 BFF 层只是做无状态的数据编排,所以我们完全可以用 FaaS 用完即毁型模型替换掉 BFF 层的 Node.js 应用,也就是最近圈子里老说的 SFF(Serverless For Frontend)。

    现在我们再串下新的请求链路逻辑。前端的一个数据请求过来,函数触发器触发我们的函数服务;我们的函数启动后,调用后端提供的元数据接口,并将返回的元数据加工成前端需要的数据格式;我们的 FaaS 函数完全就可以休息了。

  • 服务编排

    服务编排和数据编排很像,主要区别是服务编排是对云服务商提供的各种服务进行组合和加工(也就是说服务商提供了一些 API ,我们对这些 API 进行整合来实现我们想要的功能)。

    在 FaaS 出现之前,就有服务编排的概念,但服务编排受限于服务支持的 SDK 语言版本。我们要使用这些服务或 API,都要通过自己熟悉的编程语言去找对应的 SDK,在自己的代码中加载 SDK,使用秘钥调用 SDK 方法进行编排。如果没有 SDK,则需要自己根据平台提供的接口或协议实现 SDK。

    但是,有了 FaaS 之后,我们就方便很多了。假如我们服务商没有给我们提供我们熟悉的语言的 SDK,那么我们可以使用其他语言编写一个编排的程序,这个编排的程序会对服务商的服务进行编排。之后,我们再去调用这个编排的程序即可,而这个编排的程序就可以使用用完即毁的方式。比如,我们的 Web 服务需要发送验证码邮件。我们查看阿里云的邮件服务文档,发现阿里云只提供了 Java、PHP 和 Python 的 SDK,而没有 Node.js 的 SDK。这个时候,我们可以参考邮件服务的 PHP 文档,就用 PHP 的 SDK 创建一个 FaaS 服务来发送邮件(发送邮件的功能是很单一的)。

    这个也是 FaaS 的一个亮点:语言无关性。它意味着你的团队不再局限于单一的开发语言了,你们可以利用 Java、PHP、Python、Node.js 各自的语言优势,混合开发出复杂的应用。FaaS 服务编排被云服务商特别关注正是因为它具备的这种开放性。使用 FaaS 可以创造出各种各样复杂的服务编排场景,而且还与语言无关,这大大增加了云服务商各种服务的使用场景。当然,这对开发者也提出了要求,它要求开发者去更多地了解云服务商提供的各种服务。

用完即毁型比常驻进程型纯正,这需要从扩缩容角度考虑。

卷死我
dawnguo 微信支付

微信支付

dawnguo 支付宝

支付宝

  • 本文作者: dawnguo
  • 本文链接: /archives/130
  • 版权声明: 本博客所有文章除特别声明外,均采用CC BY-NC-SA 3.0 许可协议。转载请注明出处!
# Serverless # 云原生
Linux Cheat Sheet - 系统、进程、磁盘、网络、安装等
Serverless | 重新认识下 Serverless
  • 文章目录
  • 站点概览
dawnguo

dawnguo

215 日志
24 分类
37 标签
RSS
Creative Commons
© 2018 — 2025 程序锅
0%