草庐IT

AWS Lambda相关概念与实现思路

Liatsce 2023-03-28 原文

前言

本文将会介绍关于AWS Lambda(后文统称为Lambda)的基础概念以及一些编写技巧。本文适合人群为对无服务化(Serverless)感兴趣以及需要使用Lambda实现需求的工程师们。 通常,我们在实现某些业务需求时往往都会选择将其服务化,并使其对外或对内提供相应的业务服务。在服务化时,一般都会使用Web框架来做为底层进行功能实现;其次,还需要为服务化后的工程分配相应的运行载体(VM或实体服务器等)。使用的Web框架如果选型不好,则会出现类似于"实现的服务实际上就只使用到框架提供的一小部分功能"的问题,即Web框架提供的功能过于冗余,没有完全利用起来;而为工程为配运行载体则有可能会增加运维成本和资源成本。 针对上述的服务化过程中可能会出现的问题,无服务化(Serverless)可以解决的更好。通过在业务功能进行细化拆分为一个一个的方法(函数),使得最终对外或对内提供业务功能者从服务变为方法(函数)。这样的好处在于,我们无需在学习和配置底层的Web框架,而只需要专心一致的去编写业务逻辑即可。这些方法(函数)如何对外提供服务和如何接受业务请求的事情都交给无服务化(Serverless)平台去处理就好了,开发者可以不用再花时间和精力去实现这些功能。这大大节省了我们的研发压力和工期。同时,这些方法(函数)的运行方式也都完全交给无服务化(Serverless)平台去处理,包括建立,运行和回收等操作。往往无服务化(Serverless)平台还能够提供稳定的并发功能,使得编写的业务功能都能承受住大流量请求的考验。有着上述这些功能的保证,也就从而减少了增加运维成本和资源成本。 Lambda作为云平台中提供无服务化(Serverless)的先驱者,学习和使用其能够帮助我们更好的体验、理解以及设计无服务化(Serverless)。本文不是鼓励大家一定要使用AWS Lambda去做实现需求,而是鼓励大家去学习无服务化(Serverless)的相关概念和知识点。用哪家云平台的无服务化(Serverless)产品去实现功能不重要,重要的是理解其背后的设计理念和优势。 笔者也会将自己的理解在文中进行阐述,这也算是在和大家交流心得的一个过程。若文中有错误的理解和概念,请大家及时纠正;吸纳大家的建议,对于我来说也是很重要的学习过程之一。


1.相关概念

1.1 部署方式

Lambda提供了两种部署方式:

  1. 压缩包 即将源码和其依赖的第三方包都压缩在一个Zip包中,并将该包上传至Lambda计算平台上。

    其中有几点需要注意:

    1. 需要把编程语言所依赖的第三方库放入压缩包中。具体的存放方式请查看官方文档
    2. 若Lambda的项目结构与官方默认结构不同,则还需要修改Lambda入口方法的相关配置。即在Lambda控制台上的运行时设置中,将处理程序更改为<处理程序函数所在的文件名称>.<入口方法函数名>
  2. 容器 即将源码和其依赖的第三方包都打进一个容器镜像中,并将该容器镜像上传至Amazon ECR容器镜像仓库。 不同编程语言构建容器的方式详情请查看官方文档。AWS同时也提供了的各类编程语言基础镜像的下载

无论使用哪种方式进行部署,在部署后还需要为Lambda增加和修改一些重要配置:

  1. 按需修改Lambda的超时时间。如果Lambda的超时时间过长和过短都有可能会导致出现异常情况。
  2. 为Lambda配置VPC。如果Lambda需要通过网络访问AWS中的其他资源(例如EC2),则需要为其配置所属VPC。VPC子网以及安全组的配置与EC2配置方法相同,按需配置即可。
  3. 调整事件的最长保留时间。由于Lambda是基于事件驱动的方式运行的,用于触发的事件的保留时间如果不合理,则可能会导致Lambda遗漏请求等问题的发生。
  4. 调整重试次数。可以通过Lambda平台提供的重试机制来实现业务功能的重试功能。这里需要注意实现的Lambda是否支持幂等性以及其内部是否已经实现了重试机制。
  5. 启用死信队列。死信队列可以用来保存那些无法被Lambda所执行的事件(请求)。这些请求可以在后续中被用来做故障分析以及自动化处理异常请求的数据源。
  6. 为其分配IAM权限。如果Lambda需要调用或访问AWS的其他功能服务,可以通过为其配置相应的IAM即可。

1.2 调试方式

针对于不同的部署方式,调试的方式也些许不同:

  1. 压缩包 由于Lambda的实现方式实际上就是在编写方法(函数),类似于使用面向过程的方式去编写脚本。若方法(函数)中没有涉及到调用其他Lambda的逻辑,则可以通过在本地以Debug方法(函数)的形式进行调试。 其中,如果有涉及到调用AWS的相关服务,可以通过为SDK Client或API提供access key和secret_access_key来进行授权访问调用。这里需要注意一点,在正式部署之前应将关于配置认证信息的代码段删除,因为Lambda在运行时可以通过在其绑定的IAM中获取到相应的服务访问权限。这也是Lambda的优势之一,即无需将重要的账号认证信息明文暴露在编码或配置文件中。 其次,如果有调用其他自研服务的需求,则可以通过在本地搭建这些服务或直接调用相应测试环境的服务即可。 最后,通过模拟构造Lambda入口方法(函数)的入参,即可对其进行本地调试。
Tips: 这里所说的类似于编写脚本,只是便于为了让读者更好理解Lambda编码的特点。而不是指Lambda只能使用面向过程的方式去编写。实际上Lambda也可以使用面向对象的方式去编写,Lambda编码的特点在于开发者无需在去实现底层功能框架的代码逻辑,而是直接编写业务逻辑代码即可。

  1. 容器 AWS提供的基本镜像包括中Lambda运行时接口模拟器,因此是可以在本地测试Lambda的。 相关操作例如:

    1. 运行Docker映像。从项目目录中,运行docker run 命令:
    # docker run -p 9000:8080 hello-world:latest

    1. 测试Lambda函数。在新的终端窗口中,运行curl命令来调用函数:
    # curl -XPOST "http://localhost:9000/2015-03-31/functions/function/invocations" -d '{}'

    Tips: 接口模拟器下载

1.3 调用方式

1.3.1 原理

在之前的章节中也提到过,Lambda是基于事件驱动运转的,因此Lambda是需要有相应的事件来触发的。事件可以分为两大类,一类是是AWS功能服务中发生某些动作后所触发的事件,另一类是用户自定义的事件。 通过为Lambda添加相应AWS功能服务所对应的触发器,使得这些服务在发生了某些特定动作的同时也会像指定的Lambda发送相应事件。这种方式中所触发的事件内容往往都是有统一规格的。
用户自定义事件可通过使用AWS EventBridge服务来进行推送。通过将自定义的事件发送到相应的AWS EventBridge Bus中,之后EventBridge会使用绑定在该Bus上的转发规则将事件转发到相应的Lambda。在为Bus配置相应的转发规则时,AWS会自动的为Lambda添加相应的注册器。

1.3.2 同步调用

可以通过使用AWS CLI或相应语言SDK中的方法来同步调用Lambda。这种同步调用的方式需要调用方一直等待Lambda执行完后才算调用操作完成。可以通过结合上述方式,实现Lambda调用Lambda的功能,即构造Lambda调用链。

1.3.3 异步调用

如同章节1.3开头的图所示,可以通过AWS中的相关很多服务或自行构建事件驱动队列来异步调用Lambda。这种调用方式可以使得调用方无需等待Lambda执行完即可完成调用操作。但在使用异步方式调用Lambda时,要注意调用的Lambda是否满足幂等性或通过其它方式解决重复数据的问题。引入异步调用的概念后,我们还能通过配置失败队列来实现事件触发重试机制,即将处理失败的事件发送到失败队列中,并将失败队列的执行者指定为相应的Lambda即可。


2.实现思路

本章节中会介绍一些笔者设计和实现Lambda的一些实践经验。希望这些思路和经验能够帮助大家更好的设计和调用Lambda。 后续若涉及到具体的代码示例,均会使用Python来演示。虽然文章中使用的编程语言可能不是读者最熟悉的语言,但无需担心,因为示例中的逻辑流程在所有语言中是通用的。并且AWS针对每种语言所提供的SDK中,入口方法的使用都是相似的。

2.1 面向对象设计的使用

Lambda往往会选择使用编程语言中的方法(函数)来实现。例如AWS官方所给出的范例中,通过编写lambda_handler这类的方法(函数)来实现处理事件的相关逻辑。基于上述概念,大多数人通常可能就会选择使用封装方法(函数)的方式来编写Lambda,即使用面向过程设计的方法来编写。 而经过笔者实践,我们也可以使用面向对象设计的思路来编写Lambda;即对主要业务逻辑进行抽象,并且封装为功能类,最终在lambda_handler方法(函数)来创建这些类的实例对象并调用其相关的实例方法实现业务逻辑即可。使用面向对象设计来编写lambda_handler方法(函数),可以使一些底层功能逻辑复用起来更加方便(结合Lambda Layer功能),同时还能使Lambda工程有着更好的耦合度和可读性。

Tips: 当使用该思路设计和编写Lambda时,笔者想提醒大家注意以下几点:

  1. 无需过度工程化 面向对象设计通常会用来编写复杂的项目或功能框架。例如会对需求进行深层分析并抽象出底层抽象类,并使用继承或者组合的方式来设计不同类之间的关系等等。而笔者这里想提醒大家注意一个关键点,就是无服务化(Serverless)的核心概念: 无服务化(Serverless)希望我们可以将功能需求拆分的更加细致,将每一个子功能实现为一个基本单元并对外提供服务。因此,在使用面向对象设计Lambda时,应当注意Lambda中所实现的相关逻辑是否过于复杂,抽象或引入的类是否过多。如果出现这种情况,笔者建议应考虑将当前功能需求再次细分,即将当前Lambda拆分为多个Lambda,然后通过链式调用或异步服务调用的方式来将这些Lambda组合起来使用。遵循以从简原则使用面向对象的方式去编写代码即可。
  1. 功能类复用方式 引入面向对象设计后,我们可以将经常会使用到的一些功能封装为基础功能类。而这些功能类可能会在多个Lambda中都使用到。这种复用需求,在不同的部署方式有着不同的实现思路。若使用容器进行部署,需要将功能类源码拷贝到多个Lambda中。若使用Zip包部署,则有两种方式。一是类似于容器部署,将功能类源码拷贝到多个Lambda中;二是可以利用Lambda的Layer功能,将这些功能类都制作为Lambda的Layer。后续在实现Lambda时,代码依旧编写引入这些类的相关逻辑,但无需在Lambda工程结构中加入这些类的源码文件。只需在部署配置Lambda时,指定Lambda使用这些Layer即可。
  1. 抉择 这里笔者所说的抉择指的是:什么时候使用面向过程设计?什么时候使用面向对象设计? 对于这个问题,笔者提供一些思路供大家参考。面向过程设计更适合那些简单的业务逻辑,例如创建云主机,采集日志数据,变更指定数据等等。像这些能够通过封装少数方法(函数)就可以快速实现的逻辑,可以优先选择使用面向过程设计来实现。而面向对象设计适合的业务场景有以下几个特点: 1.相对复杂的业务需求;2.若该业务需求灵活多变,需要Lambda能够快速支持;3. Lambda可能会有多个人同时负责开发和维护。如果业务场景有上述的特点,可以优先考虑使用面向对象设计来实现。

2.2 日志打印

由于Lambda是一种即用即开的服务类型,因此Lambda服务不提供用于数据永久落地的存储设备。基于上述情况,使用相关日志文件来记录Lambda的过程日志就无法实现了。 但Lambda的屏幕打印输出会被采集到Amazon CloudWatch服务中,因此我们可以利用这一点来变向实现我们的日志记录需求。可以通过将原本输出至指定日志文件的日志流,更改为输出到stdout或stderr中。这样日志流就会被自动的采集到Amazon CloudWatch服务中了。因为Amazon CloudWatch服务本身可以用来做日志分析,这样还顺便解决了日志采集和分析的需求。 例如使用Python自带的logging模块进行日志记录:

import logging import sys logger = logging.getLogger('zmebula') formatter = logging.Formatter( "%(asctime)s [%(levelname)s] %(funcName)s:%(lineno)d %(message)s" ) sh = logging.StreamHandler(stream=sys.stdout) sh.setFormatter(formatter) sh.setLevel(logging.INFO) logger.addHandler(sh)

2.3 异常处理

为了保证项目能够长时间稳定的运行,通常会选择在捕获到异常后,选择对其进行记录并进行临时处理,以保证程序不会因抛出异常而意外停止。 AWS判断一个Lambda是否执行成功,主要是监控其有无正常退出。如果Lambda正常退出,则视为执行成功;若有异常抛出导致程序意外停止,则认为其执行失败。 因此,建议在处理Lambda中的异常时,在进行异常记录后,可以再次将其抛出,以来通知AWS当前Lambda执行失败了。在这样设计和实现后,就会避免AWS监控平台上出现Lambda执行结果误报的问题,同时还能对于Lambda真实的运行状态进行统计和分析。

2.4 权限配置

由于Lambda为AWS的产物,因此其和AWS其他服务的结合使用是极其便利的,AWS官方也推荐使用者将Lambda和AWS的其他服务配合使用。 既然要调用AWS的其他服务,那么就必然会牵扯到调用这些服务的权限问题。在一般开发中,我们会通过在调用AWS的SDK的相关逻辑中加入身份认证信息(ak,ask)以表明操作的用户身份。同时会为这个用户绑定相应的AWS IAM,以保证其拥有操作指定服务的权限。 同样,在Lambda中也会通过调用AWS的SDK来操作其他服务。但有一点不同的是,在Lambda无需录入相关用户的身份信息(ak,ask)。可以通过直接为Lambda绑定相应的AWS IAM来实现权限分配。这样的做法可以防止用户身份信息(ak,ask)通过硬编码或明文配置文件的方式暴露在Lambda中。

Tips: 对于配置AWS IAM,笔者这里想分享给大家一些实践的经验。了解和配置过AWS IAM的人应该都清楚其中的痛点,由于AWS对于其服务操作权限的划分过于细致,会导致在配置IAM时会因不熟悉服务的权限划分而出现权限遗漏现象,从而导致Lambda因权限不足而无法正常运行。针对上述问题,笔者这里推荐给大家几个解决该问题的思路:

  1. 开Case 解铃还需系铃人,直接询问AWS的专业工程师是最快也是直接的方法。通过在Case中描述清楚你的Lambda都需要那些服务的操作权限,AWS会将Case指向专业处理Lambda的工程师们。他们对于Lambda的熟悉程度是高于我们的,因此可以他们的帮助来快速解决这个问题。同时这也是合理利用云厂商提供的便利服务的一种体现。
  1. 善用控制台的提示 现阶段,在AWS IAM的控制台进行相关配置时,系统会自动把某个权限所需的其他必备权限罗列出来。因此在配置时,需留意将系统提供的这些必要权限加上。
  1. 被动查找 所谓的被动查找,实际上是指在不绑定任何AWS IAM的情况下,直接运行Lambda。这时Lambda一定会因为没有某些服务的操作权限而报错(前提是正确记录了这些异常),此时就可以根据返回的这些错误信息来为Lambda添加所需的权限了。这种方法的弊端在于效率极低,调试Lambda的次数会随着Lambda中所涉及到的服务调用的数量而增多;而优势在于这样的做法能够产出一个最小需求的权限集合,以此能够保证Lambda指定的操作不会越界。

2.5 数据落地

这里将会分为数据永久落地和数据临时落地两种情况进行讨论。 对于Lambda产出数据的永久落地,建议使用Lambda与AWS相关的数据库服务来解决数据永久落地的需求。在之前的章节中也曾提到过,Lambda本身是不提供用于数据永久落地的存储设备,因此需要借助外界的服务实现该需求。 对于在Lambda运行中所产出的临时性数据落地,AWS提供了用于缓存临时数据的存储。Lambda有权访问/tmp目录中的本地存储,因此Lambda中如果有需要缓存的逻辑,则可以利用/tmp目录来实现。

2.6 提供Web Service

在前言章节中层提到过,Lambda是无服务化(Serverless)实现的一种途径。将每一个子功能看作服务并对外提供业务。对外提供功能(服务)的形式通常都会选择使用Web Service的形式,即通过HTTP/HTTPS协议来对外提供业务功能。 对于Lambda而言,实现上述需求有两种方式:

  1. 结合AWS API Gateway 通过和AWS API Gateway服务结合使用,可以为Lambda提供创建、发布、维护、监控和保护任意规模的REST、HTTP和WebSocket API。
  2. 内置HTTPS终端节点 通过使用Lambda的内置HTTPS终端节点功能,也可以使Lambda对外能够提供Web Service。

总结

笔者想通过本文中所介绍的Lambda相关概念以及一些经过笔者实践过的设计经验来告诉大家,无服务化(Serverless)实现起来并没有我们想象中的那么困难了。无服务化(Serverless)可以使我们更加关注于业务逻辑的实现,这一点在实际工作中的体现就是:

  1. 与公司的成本控制和业务发展结合度高 在无服务化(Serverless)的实践中,更多的人力可以用于去实现业务逻辑,而不再像微服务化时需要有专业的团队专注于设计和实现底层框架。这大大能够节省公司的人力成本,而且使得研发团队能更够专注于业务上的功能开发。
  2. 减少开发成本 使用无服务化(Serverless)后,开发人员不用再考虑底层框架的相关实现。这一点笔者深有体会,平常在实现新工程时,我们需要先将底层框架实现好,然后在其上实现业务逻辑。即使能够通过拷贝原有工程的底层框架部分,但拷贝后的细节配置更改依旧会消耗时间和精力。但这些问题在无服务化(Serverless)中就不复存在了,开发人员从一开始就可以直接针对业务需求进行设计和研发,无需再花费时间去搭建,对接底层框架。
当然,不是所有的场景都适合无服务化(Serverless)。推行无服务化(Serverless)一个是需要合适的业务场景,一个是需要整个研发团队有着不错的接受度和设计理念。但这不影响我们去学习这一概念,通过学习新的概念和其原理,也能够帮助我们在设计其他事物中起到参考和拓展思路的作用。

有关AWS Lambda相关概念与实现思路的更多相关文章

  1. ruby - 如何根据特征实现 FactoryGirl 的条件行为 - 2

    我有一个用户工厂。我希望默认情况下确认用户。但是鉴于unconfirmed特征,我不希望它们被确认。虽然我有一个基于实现细节而不是抽象的工作实现,但我想知道如何正确地做到这一点。factory:userdoafter(:create)do|user,evaluator|#unwantedimplementationdetailshereunlessFactoryGirl.factories[:user].defined_traits.map(&:name).include?(:unconfirmed)user.confirm!endendtrait:unconfirmeddoenden

  2. ruby-on-rails - 相关表上的范围为 "WHERE ... LIKE" - 2

    我正在尝试从Postgresql表(table1)中获取数据,该表由另一个相关表(property)的字段(table2)过滤。在纯SQL中,我会这样编写查询:SELECT*FROMtable1JOINtable2USING(table2_id)WHEREtable2.propertyLIKE'query%'这工作正常:scope:my_scope,->(query){includes(:table2).where("table2.property":query)}但我真正需要的是使用LIKE运算符进行过滤,而不是严格相等。然而,这是行不通的:scope:my_scope,->(que

  3. 华为OD机试用Python实现 -【明明的随机数】 2023Q1A - 2

    华为OD机试题本篇题目:明明的随机数题目输入描述输出描述:示例1输入输出说明代码编写思路最近更新的博客华为od2023|什么是华为od,od薪资待遇,od机试题清单华为OD机试真题大全,用Python解华为机试题|机试宝典【华为OD机试】全流程解析+经验分享,题型分享,防作弊指南华为o

  4. 基于C#实现简易绘图工具【100010177】 - 2

    C#实现简易绘图工具一.引言实验目的:通过制作窗体应用程序(C#画图软件),熟悉基本的窗体设计过程以及控件设计,事件处理等,熟悉使用C#的winform窗体进行绘图的基本步骤,对于面向对象编程有更加深刻的体会.Tutorial任务设计一个具有基本功能的画图软件**·包括简单的新建文件,保存,重新绘图等功能**·实现一些基本图形的绘制,包括铅笔和基本形状等,学习橡皮工具的创建**·设计一个合理舒适的UI界面**注明:你可能需要先了解一些关于winform窗体应用程序绘图的基本知识,以及关于GDI+类和结构的知识二.实验环境Windows系统下的visualstudio2017C#窗体应用程序三.

  5. MIMO-OFDM无线通信技术及MATLAB实现(1)无线信道:传播和衰落 - 2

     MIMO技术的优缺点优点通过下面三个增益来总体概括:阵列增益。阵列增益是指由于接收机通过对接收信号的相干合并而活得的平均SNR的提高。在发射机不知道信道信息的情况下,MIMO系统可以获得的阵列增益与接收天线数成正比复用增益。在采用空间复用方案的MIMO系统中,可以获得复用增益,即信道容量成倍增加。信道容量的增加与min(Nt,Nr)成正比分集增益。在采用空间分集方案的MIMO系统中,可以获得分集增益,即可靠性性能的改善。分集增益用独立衰落支路数来描述,即分集指数。在使用了空时编码的MIMO系统中,由于接收天线或发射天线之间的间距较远,可认为它们各自的大尺度衰落是相互独立的,因此分布式MIMO

  6. 【Java入门】使用Java实现文件夹的遍历 - 2

    遍历文件夹我们通常是使用递归进行操作,这种方式比较简单,也比较容易理解。本文为大家介绍另一种不使用递归的方式,由于没有使用递归,只用到了循环和集合,所以效率更高一些!一、使用递归遍历文件夹整体思路1、使用File封装初始目录,2、打印这个目录3、获取这个目录下所有的子文件和子目录的数组。4、遍历这个数组,取出每个File对象4-1、如果File是否是一个文件,打印4-2、否则就是一个目录,递归调用代码实现publicclassSearchFile{publicstaticvoidmain(String[]args){//初始目录Filedir=newFile("d:/Dev");Datebeg

  7. ruby - Arrays Sets 和 SortedSets 在 Ruby 中是如何实现的 - 2

    通常,数组被实现为内存块,集合被实现为HashMap,有序集合被实现为跳跃列表。在Ruby中也是如此吗?我正在尝试从性能和内存占用方面评估Ruby中不同容器的使用情况 最佳答案 数组是Ruby核心库的一部分。每个Ruby实现都有自己的数组实现。Ruby语言规范只规定了Ruby数组的行为,并没有规定任何特定的实现策略。它甚至没有指定任何会强制或至少建议特定实现策略的性能约束。然而,大多数Rubyist对数组的性能特征有一些期望,这会迫使不符合它们的实现变得默默无闻,因为实际上没有人会使用它:插入、前置或追加以及删除元素的最坏情况步骤复

  8. ruby - "public/protected/private"方法是如何实现的,我该如何模拟它? - 2

    在ruby中,你可以这样做:classThingpublicdeff1puts"f1"endprivatedeff2puts"f2"endpublicdeff3puts"f3"endprivatedeff4puts"f4"endend现在f1和f3是公共(public)的,f2和f4是私有(private)的。内部发生了什么,允许您调用一个类方法,然后更改方法定义?我怎样才能实现相同的功能(表面上是创建我自己的java之类的注释)例如...classThingfundeff1puts"hey"endnotfundeff2puts"hey"endendfun和notfun将更改以下函数定

  9. ruby-on-rails - 在具有 ActiveRecord 条件的相关模型中按字段排序 - 2

    我正在尝试按Rails相关模型中的字段进行排序。我研究的所有解决方案都没有解决如果相关模型被另一个参数过滤?元素模型classItem相关模型:classPriority我正在使用where子句检索项目:@items=Item.where('company_id=?andapproved=?',@company.id,true).all我需要按相关表格中的“位置”列进行排序。问题在于,在优先级模型中,一个项目可能会被多家公司列出。因此,这些职位取决于他们拥有的company_id。当我显示项目时,它是针对一个公司的,按公司内的职位排序。完成此任务的正确方法是什么?感谢您的帮助。PS-我

  10. ruby - 实现k最近邻需要哪些数据? - 2

    我目前有一个reddit克隆类型的网站。我正在尝试根据我的用户之前喜欢的帖子推荐帖子。看起来K最近邻或k均值是执行此操作的最佳方法。我似乎无法理解如何实际实现它。我看过一些数学公式(例如k表示维基百科页面),但它们对我来说并没有真正意义。有人可以推荐一些伪代码,或者可以查看的地方,以便我更好地了解如何执行此操作吗? 最佳答案 K最近邻(又名KNN)是一种分类算法。基本上,您采用包含N个项目的训练组并对它们进行分类。如何对它们进行分类完全取决于您的数据,以及您认为该数据的重要分类特征是什么。在您的示例中,这可能是帖子类别、谁发布了该项

随机推荐