Serverless¿
我们近一两年来每天在各种公众号、前端大会上听见这个词。一听到什么serverless,什么微服务,总感觉自己像是一个外星科技的观光客,外星人的东西再好,我们单位没有这种基础设施我该怎么用呢?我一个小小的切图仔,难道还要我造一堆serverless的设施出来?
既然地球没有,那我们只能去外星偷学一波技能,正所谓“师夷长技”,没准哪一天地球招人的时候也需要有serverless开发经验的工程师呢。
什么是serverless?
晦涩的外星语言我就不在这里多做赘述了,相信大家对什么BaaS, FaaS这种词都看了不下数十遍了,用地球人的话来说就是:有了serverless,大家在写应用时再也不用去关心什么服务器、后端运维了,我们只需要专注在写业务逻辑代码就行了。下面再重述一下几条老生常谈的优势
- 应用开发迅速
- 服务弹性伸缩
- 按用量收费
听到这可能有些人就会想了,serverless这么厉害,是不是后端和运维同事都得下岗了呢?这个问题可以说是见仁见智的,我可以表达一个个人的想法:在大规模使用serverless架构的前提下,对于有serverless基础设施的公司来说,serverless能将后端从业务中解放出来,更加明确地划分不同工程师的职责;而对于依赖于他人serverless服务的公司来说,JS全栈工程师就已经足以胜任所有业务开发的职责了。
Serverless服务商选择
在真正体验之前,先来对比一下现有的几家Serverless服务商
- Aws Lambda - 亚马逊的Lambda最早推出于2014年,是最为著名的serverless计算服务提供商,一度成为serverless的代名词。Netflix(美国爱奇艺)就是该服务较为出名的客户案例。
- 免费额度:每月
100万
次请求以及400000GB-秒
的计算时间 - 收费:
$0.00001667 / GB-秒
- 免费额度:每月
-
Azure Functions - 微软于2016年推出了他们的serverless解决方案Azure Functions。
- 免费额度:每月
100万
次请求以及400000GB-秒
的计算时间 - 收费:
$0.000016 / GB-秒
- 免费额度:每月
-
Google Cloud Functions - 谷歌于2017年推出的解决方案,早期落后于亚马逊与微软,但是在近年来修复了不少问题,有迎头赶上的趋势。
- 免费额度:每月
200万
次请求以及400000GB-秒
的计算时间 - 收费:
$0.0000004 / GB-秒
(额外征收内存
与CPU
的费用)
- 免费额度:每月
- IBM Cloud Functions - IBM最新推出的基于开源Serverless项目Apache OpenWhisk的解决方案。
- 免费额度:每月
100万
次请求以及400000GB-秒
的计算时间 - 收费:
$0.000017 / GB-秒
- 免费额度:每月
- 阿里云函数计算 - 阿里云最早于2017年亮相的serverless项目。
- 免费额度:每月
100万
次请求以及400000GB-秒
的计算时间 - 收费:
$0.00001617 / GB-秒
(按当前汇率 1 : 6.87计算)
- 免费额度:每月
从价格
的角度来看,较为贵的一家是谷歌,尽管提供了200万
次的免费请求额度,谷歌对于内存
与CPU
的额外收费会显著提高使用他家服务的开支。另外四家的价格都是较为接近的,其中以微软最低,IBM最高,亚马逊和阿里云处于中游。
从实用
的角度来看,亚马逊和微软的服务仍然以完善的设施(触发器种类多、支持语言多...) 和丰富的社区支持在多数评测中占据了上风,而谷歌与刚崭露头角的IBM、阿里云依然是处于跟跑的状态。这五家中Lambda可以说是体验serverless的最佳选择了。
Serverless应用框架
我们选择serverless框架来快速创建、部署Lambda服务。
这里的serverless指的是一个在GitHub上超过3万星的一个cli工具。通过serverless cli,我们可以快速生成Lambda服务模版,标准化、工程化服务的开发以及一键部署服务至多套的环境与节点,极大地缩短了服务开发至上线的时间。
如果你是一个更加信赖纯净的AWS设施的人,愿意跟随着原汁原味的AWS Lambda开发文档来开发的话,那也是极好的。只是可能我的服务今天就上线了,你的要等到后天。
在Lambda上搭建服务
目标
接下来的两个小时,我们要在Lambda上部署一套常见的用户服务,包含以下四个接口
/api/user/signup
- 创建一个新用户并录入数据库/api/user/login
- 登入并返回JSON Web Token来让用户访问私有接口/api/public
- 公共接口,无须登入的用户也可访问/api/private
- 私有接口,只有登入后的用户才能访问
准备材料
- 通畅的全球互联网
- 一个可用的AWS账号
- 根据serverless文档完成前两步
npm install -g serverless
设置AWS Credentials
创建工程
我们选择的语言是JS,数据库是DynamoDb,从serverless的示例库中很快可以找到这样的模版aws-node-rest-api-with-dynamodb
复制模版至本地作为起步工程
serverless install -u https://github.com/serverless/examples/tree/master/aws-node-rest-api-with-dynamodb
复制代码
这个工程包含了一个Todo列表的CRUD操作服务。核心文件有:
-
serverless.yml
定义了该服务所提供的Lambda函数
、触发函数的触发器
以及运行该函数所需要的其他AWS资源
。 -
package.json
定义了该服务所依赖的其他库。 -
todos
目录下包含了所有的函数文件。我们可以看到函数都是较为直白的,每一个文件都是类似以下的结构:const AWS = require('aws-sdk'); // 引入AWS SDK const dynamoDb = new AWS.DynamoDB.DocumentClient(); // 建立dynamoDb实例 // 通过 module.exports 来导出该函数 module.exports.create = (event, context, callback) => { const data = JSON.parse(event.body); // 解析event来获得请求数据 /* 业务逻辑 */ callback(); // 用callback来返回响应数据 } 复制代码
当我们运行npm install
与serverless deploy
将该起步工程部署到云端后,就可以通过Api地址(例:xxxxxx.execute-api.us-east-1.amazonaws.com/dev/todos)来运行和访问这些函数。
创建与定义函数
根据这个工程原有的函数 ,创建类似的函数文件并非难事,我们在工程中创建以下4个文件:
- user/signup.js
- user/login.js
- user/public.js
- user/private.js
但仅仅创建函数文件是不够的,我们需要同时在serverless.yml
中为这几个函数添加定义。以signup
函数为例,在functions
中添加以下内容:
signup:
handler: user/signup.signup #定义了函数文件的路径
events:
- http: #定义了函数触发器种类为http (AWS API Gateway)
path: api/user/signup #定义了请求路径
method: post #定义了请求method种类
cors: true #开启跨域
复制代码
这样我们就完整地定义了4个函数。接下来我们来看这四个函数具体的实现方法。
Public函数
GET
返回一条无须登陆即可访问的信息
1. 返回消息
public
函数是4个函数中最为简易的一个,因为该函数是完全公开的,我们不需要对该函数做任何校验。如下,简单地返回一条信息便可:
// user/public.js
module.exports.public = (event, context, callback) => {
const response = {
statusCode: 200,
body: JSON.stringify({
message: '任何人都可以阅读此条信息。'
})
}
return callback(null, response);
};
复制代码
注:
callback
第一个参数传入的为错误,第二个参数传入的为响应数据。
2. 部署及访问服务
- 执行以下命令来部署
public
函数
或# 部署单个函数 serverless deploy function -f public 复制代码
# 部署所有函数 serverless deploy 复制代码
- 在浏览器中直接输入API地址或用cURL工具执行以下命令来发送请求(替换成你的API地址,API地址可在运行
serverless deploy
后的log中或在AWS API Gateway控制台
-阶段(stage)
中找到)curl -X GET https://xxxxxx.execute-api.us-west-2.amazonaws.com/dev/api/public 复制代码
- 返回数据
{ "message": "任何人都可以阅读此条信息。" } 复制代码
Sign up函数
POST
创建一个新用户并录入数据库,返回成功或失败信息
1. 定义资源
signup
函数的运行需要DynamoDB
这一资源,所以第一步我们需要在serverless.yml
文件中对resources
进行如下修改
# serverless.yml
resources:
Resources:
UserDynamoDbTable:
Type: 'AWS::DynamoDB::Table' #资源种类为DynamoDB表
DeletionPolicy: Retain #当删除CloudFormation Stack(serverless remove)时保留该表
Properties:
AttributeDefinitions: #定义表的属性
-
AttributeName: username #属性名
AttributeType: S #属性类型为字符串
KeySchema: #描述表的主键
-
AttributeName: username #键对应的属性名
KeyType: HASH #键类型为哈希
ProvisionedThroughput: #表的预置吞吐量
ReadCapacityUnits: 1 #读取量为1单元
WriteCapacityUnits: 1 #写入量为1单元
#用serverless变量来定义表名,表名为环境变量中的定义的DYNAMODB_TABLE
TableName: ${self:provider.environment.DYNAMODB_TABLE}
复制代码
resources
一栏中填写的内容是使用yaml
语法写的AWS CloudFormation的模版。
DynamoDB表在CloudFormation中更为详细定义文档请参考链接。
2. 获取请求数据
signup
是一个方法为POST
的接口,因此需要从触发事件的body
中获取请求数据。
// user/signup.js
module.exports.signup = (event, context, callback) => {
// 获取请求数据并解析JSON字符串
const data = JSON.parse(event.body);
const { username, password } = data;
/*
...
校验 username 与 passowrd
*/
}
复制代码
3. 录入用户至DynamoDB
获取完了请求数据后,我们需要构造出新用户的数据,并把数据录入DynamoDB
// user/signup.js
// 引入nodejs加密库
const crypto = require('crypto');
// 创建dynamoDB实例
const AWS = require('aws-sdk');
const dynamoDb = new AWS.DynamoDB.DocumentClient();
module.exports.signup = (event, context, callback) => {
// ...获取并校验 username 与 password
// 生成新用户的数据
// 生成salt来确保哈希后密码的唯一性
const salt = crypto.randomBytes(16).toString('hex');
// 用sha512哈希函数加密,生成仅可单向验证的哈希密码
const hashedPassword = crypto.pbkdf2Sync(password, salt, 10000, 512, 'sha512').toString('hex');
const timestamp = new Date().getTime(); // 生成当前时间戳
const params = {
TableName: process.env.DYNAMODB_TABLE, // 从环境变量中获取DynamoDB表名
Item: {
username, // 用户名
salt, // 保存salt用于登陆时单向校验密码
password: hashedPassword, // 哈希密码
createdAt: timestamp, // 生成时间
updatedAt: timestamp // 更新时间
}
}
// 录入至dynamoDb
dynamoDb.put(params, (error) => {
// 返回失败信息
if (error) {
// log错误信息,可在AWS CloudWatch服务中查看
console.error(error);
callback(null, {
statusCode: 500,
body: JSON.stringify({
message: '创建用户失败!'
})
});
} else {
// 返回成功信息
callback(null, {
statusCode: 200,
body: JSON.stringify({
message: '创建用户成功!'
})
});
}
}
复制代码
DynamoDB在Nodejs中更详细的CRUD操作文档请参考链接
4. 部署及访问服务
- 执行以下命令来部署
signup
函数
或# 部署单个函数 serverless deploy function -f signup 复制代码
# 部署所有函数 serverless deploy 复制代码
- 用
cURL
工具执行以下命令来发送请求(替换成你的API地址,API地址可在运行serverless deploy
后的log中或在AWS API Gateway控制台
-阶段(stage)
中找到)curl -X POST https://xxxxxx.execute-api.us-west-2.amazonaws.com/dev/api/user/signup --data '{ "username": "new_user", "password": "12345678" }' 复制代码
- 返回数据
{ "message": "success" } 复制代码
Login函数
GET
校验用户名密码并返回JSON Web Token来让登陆用户访问私有接口
1. 设置JSON Web Token
在用户调用了Login接口并通过验证后,我们需要为用户返回一个JSON Web Token
,以供用户来调用需要权限的服务。设置JSON Web Token需要以下几步操作:
npm install jsonwebtoken --save
安装jsonwebtoken库并添加至项目依赖- 在项目中添加一个
secret.json
文件来存放密钥,这里我们采用对称加密的方式来定义一个私有密钥。// secret.json { "secret": "私有密钥" } 复制代码
- 将私有密钥定义至环境变量,以供函数访问。在
serverless.yml
的provider
下作如下变更# serverless.yml provider: environment: # 使用serverless变量语法将文件中的密钥赋值给环境变量PRIVATE_KEY PRIVATE_KEY: ${file(./secret.json):secret} 复制代码
2. 获取请求数据
login
是一个方法为GET
的接口,因此需要从触发事件的queryStringParameters
中获取请求数据。
// user/login.js
module.exports.login = (event, context, callback) => {
// 获取请求数据
const { username, password } = event.queryStringParameters;
/*
...
校验 username 与 passowrd
*/
}
复制代码
3. 验证账号密码并返回JSON Web Token
// user/login.js
const crypto = require('crypto');
const jwt = require('jsonwebtoken');
const AWS = require('aws-sdk');
const dynamoDb = new AWS.DynamoDB.DocumentClient();
module.exports.login = (event, context, callback) => {
// ...获取并校验 username 与 password
// 验证账号密码并返回JSON Web Token
// 构造DynamoDB请求数据,根据主键username获取数据
const params = {
TableName: process.env.DYNAMODB_TABLE,
Key: {
username
}
};
// 从DynamoDB中获取数据
dynamoDb.get(params, (error, data) => {
if (error) {
// log错误信息,可在AWS CloudWatch服务中查看
console.error(error);
// 返回错误信息
callback(null, {
statusCode: 500,
body: JSON.stringify({
message: '登陆失败!'
})
});
return;
}
// 从回调参数中获取DynamoDB返回的用户数据
const user = data.Item;
if (
// 确认username存在
user &&
// 确认哈希密码匹配
user.password === crypto.pbkdf2Sync(password, user.salt, 10000, 512, 'sha512').toString('hex')
) {
// 返回登陆成功信息
const response = {
statusCode: 200,
body: JSON.stringify({
username, // 返回username
// 返回JSON Web Token
token: jwt.sign(
{
username // 嵌入username数据
},
process.env.PRIVATE_KEY // 使用私有密钥签发token
)
})
};
callback(null, response);
} else {
// 返回错误信息
callback(null, {
statusCode: 401,
body: JSON.stringify({
message: '用户名或密码错误!'
})
});
}
});
};
复制代码
4. 部署及访问服务
- 执行以下命令来部署
login
函数
或# 部署单个函数 serverless deploy function -f login 复制代码
# 部署所有函数 serverless deploy 复制代码
- 在浏览器中直接输入API地址或用cURL工具执行以下命令来发送请求(替换成你的API地址,API地址可在运行
serverless deploy
后的log中或在AWS API Gateway控制台
-阶段(stage)
中找到)curl -X GET 'https://xxxxxx.execute-api.us-west-2.amazonaws.com/dev/api/user/login?username=new_user&password=12345678' 复制代码
- 返回数据
{ "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Im5ld191c2VyIiwiaWF0IjoxNTYxODI1NTgyfQ.Iv0ulooGayulxf_MkkpBO1xEw1gilThT62ysuz-rQE0", "username": "new_user" } 复制代码
Auth函数
校验请求中所包含的JSON Web Token是否有效
在编写private
函数之前,我们需要提供另一个函数auth
来校验用户提交请求中的JSON Web Token是否与我们所签发的一致。
1. 创建函数
- 在项目中添加
user/auth.js
文件 - 在
serverless.yml
的functions
中添加以下内容:auth: handler: user/auth.auth # auth是一个仅会在服务内被调用的函数,因此没有任何触发器 复制代码
2. 生成包含IAM权限策略的响应信息
为了能够让AWS API Gateway
触发器正确地识别函数有无权限执行,我们必须在auth
函数中返回一个含IAM ( AWS 服务与权限管控系统) 权限策略信息的响应数据,来使得有权限的函数可以通过AWS API Gateway
成功触发。在user/auth.js
内定义一个如下的方法:
// user/auth.js
const generatePolicy = (principalId, effect, resource) => {
const authResponse = {};
authResponse.principalId = principalId; // 用于标记用户身份信息
if (effect && resource) {
const policyDocument = {};
policyDocument.Version = '2012-10-17'; // 定义版本信息
policyDocument.Statement = [];
const statementOne = {};
statementOne.Action = 'execute-api:Invoke'; // 定义操作类型,这里为API调用操作
statementOne.Effect = effect; // 可用值为ALLOW或DENY,用于指定该策略所产生的结果是允许还是拒绝
statementOne.Resource = resource; // 传入ARN(AWS资源名)来指定操作所需要的资源
policyDocument.Statement[0] = statementOne;
authResponse.policyDocument = policyDocument; // 将定义完成的策略加入响应数据
}
return authResponse;
};
复制代码
关于IAM策略更为详细的配置文档请查看链接
3. 解析并校验JSON Web Token
我们在解析JSON Web Token时默认请求遵循OAuth2.0中的Bearer Token格式。
// user/auth.js
const jwt = require('jsonwebtoken');
/*
...定义generatePolicy方法
*/
module.exports.auth = (event, context, callback) => {
// 获取请求头中的Authorization
const { authorizationToken } = event;
if (authorizationToken) {
// 解析Authorization
const split = event.authorizationToken.split(' ');
if (split[0] === 'Bearer' && split.length === 2) {
try {
const token = split[1];
// 使用私有密钥校验JSON Web Token
const decoded = jwt.verify(token, process.env.PRIVATE_KEY);
// 使用generatePolicy生成包含允许API调用的IAM权限策略的响应数据
const response = generatePolicy(decoded.username, 'Allow', event.methodArn);
return callback(null, response);
} catch (error) {
// JSON Web Token 校验失败,返回错误
return callback('Unauthorized');
}
} else {
// Authorization 格式校验失败,返回错误
return callback('Unauthorized');
}
} else {
// 请求头未含Authorzation,返回错误
return callback('Unauthorized');
}
};
复制代码
Private函数
GET
返回一条需要登陆才可访问的信息
private
函数的实现与之前的public
函数十分类似,唯一的区别就是我们需要在函数的http (AWS API Gateway)
触发器中加入刚刚定义的auth
作为权限校验函数。
1. 设置authorizer
在serverless.yml
中对先前定义的private
函数作如下变更:
# serverless.yml
functions:
private:
handler: user/private.private
events:
- http:
path: api/private
method: get
authorizer: auth #设置authorizer为auth函数
cors: true
复制代码
2. 返回消息
// user/private.js
module.exports.private = (event, context, callback) => {
// 从触发事件中获取请求的用户信息
const username = event.requestContext.authorizer.principalId;
// 返回消息
const response = {
statusCode: 200,
body: JSON.stringify({
message: `你好,${username}!只有登陆后的用户才可以阅读此条信息。`
})
}
return callback(null, response);
};
复制代码
3. 部署及访问服务
- 执行以下命令来部署
private
函数
或# 部署单个函数 serverless deploy function -f private 复制代码
# 部署所有函数 serverless deploy 复制代码
- 用cURL工具执行以下命令来发送请求(替换成你的API地址,API地址可在运行
serverless deploy
后的log中或在AWS API Gateway控制台
-阶段(stage)
中找到)curl -X GET -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Im5ld191c2VyIiwiaWF0IjoxNTYxODI1NTgyfQ.Iv0ulooGayulxf_MkkpBO1xEw1gilThT62ysuz-rQE0" https://xxxxxx.execute-api.us-west-2.amazonaws.com/dev/api/private 复制代码
- 返回数据
{ "message": "你好,new_user!只有登陆后的用户才可以阅读此条信息。" } 复制代码
小结
用了什么?
- serverless cli
- AWS Lambda
- AWS API Gateway
- AWS CloudWatch
- DynamoDB
- OAuth2.0
做了什么?
- 开发与部署了一套API服务
- 服务提供了用户注册、登入接口
- 服务提供了完全开放接口与仅登陆用户可访问接口
完整工程
- 该项目完整的工程可在github.com/yuanfux/aws…中查看
- 运行以下命令可以快速部署该工程并访问服务
serverless install -u https://github.com/yuanfux/aws-lambda-user
cd aws-lambda-user
npm install
serverless deploy
思考
我们看到了AWS Lambda的实用、便捷与低价,但我们能否看到隐藏在其背后的一些问题。
实用¿
-
代码维护
函数式的开发模式注定了代码之间的复用与共享会成为一个难题,也注定了代码量会随着服务的增多而膨胀。serverless会使得函数数量与代码量呈线性增长的关系,如下图
当服务达到一定数量后,维护一个无限膨胀的代码库所需要的额外人力与开支也是不可小视的。
-
冷启动
冷启动也是一个老生常谈的话题了,用简单的话来说就是当你的函数一段时间未被运行后,系统就会回收运行你函数的容器资源。这样带来负面影响就是,当下一次调用这个函数时,就需要重新配置一个容器来运行你的函数,结果就是函数的调用会被延迟。来看一下函数调用时间间隔与冷启动概率的关系:
那么具体的延迟时间是多少呢?延迟时间受许多因素的影响,比如你选择的编程语言、函数的内存配置、函数的文件大小等等。但是一个较为普遍的建议就是 Lambda 不适合用作对延迟极其敏感的服务( < 50ms)。
便捷¿
-
本地开发
运行Lambda函数依赖于许多外部的库和应用(aws-sdk, API Gateway、DynamoDB...),因此想要在一个完全本地的环境运行这样的函数是十分困难的。如果我们每次修改函数后都需要部署并依赖于 AWS CloudWatch 中输出的运行日志来调试与开发 Lambda 函数,那想必效率是极低的。幸好
serverless cli
提供了一定的插件来支持基本的本地开发(serverless-offline、serverless-dynamodb-local...)。但不用serverless cli
开发Lambda的用户可能就需要研读AWS-SAM文档并走过一段更为漫长的配置过程了。 -
迁移
通过开发AWS Lambda你可能已经发现了,我们所写的代码与AWS这个云服务商是具有强关联性的。尽管有
serverless cli
这种通用的serverless应用框架来帮助我们抹平不同服务商之间的代码差异,想从一个服务商迁移至另一个服务商依然是一件繁重的体力劳动,甚至包含着大量的代码重构。用serverless就像抽烟,可能一开始你享受到了烟的美妙,觉得抽几根无所谓,想戒时定然能戒。但随着你越陷越深,你会发现戒烟是一个相当痛苦的过程。
低价¿
-
计价方式
AWS Lambda收费的最小单位是100ms,也就是意味着你的函数哪怕只执行了1ms也会当作100ms来计费。这种计费方式甚至会导致使用高内存的函数甚至比低内存的要便宜!我们来看下AWS Lambda的计费表:
举一个较为极端的例子:假设我们设置了一个内存为448MB的函数,它运行时间为101ms,那么每次执行我们都需要支付0.000000729 x 2 = $0.000001458。而如果我们将这个函数的内存提高到512MB,使它的运行时间降低100ms以内,那么每次执行我们只需要支付$0.000000834。仅仅是一个设置,我们就降低了整整(1458 - 834) / 1458 ≈ 42.8% 的成本!
找到性价比最高的内存设置意味着额外的工作量,很难想象AWS在这个问题上居然没有为客户提供一个合理的解决方案。
-
捆绑消费
在使用AWS Lambda的时几乎所有的周边服务(API Gateway、 CloudWatch、DynamoDB...)都是需要额外收费的。其中一个很明显的特征就是捆绑消费,你可能很难想象 CloudWatch 是在使用 Lambda 时被强制使用的一个服务;而 API Gateway 也是在搭建http服务时几乎无法逃过一个收费站,其$3.5/百万次请求的高额价格甚至远远高于使用 Lambda 的价格。
-
成长型吸血鬼
小明刚刚迁移了他每个月花费$5搭建在某VPS商的个人博客到AWS Lambda上,他发现所有的服务都没有超过AWS的免费线,Lambda为小明每个月省下了$5。对于像小明这样的小流量、小内存服务来说,Lambda的的确确会省下一笔相当可观的成本;但对于占用大流量、大内存的服务来说,Lambda的按调用量收费反而会在无形之间累加出一笔高额费用。使用Lambda就像在住酒店,而租服务器则像租房。酒店设施齐全且便捷,偶尔住几天可能比租房还便宜,但每天每夜住咱也住不起。
真香
虽然Lambda有以上值得权衡的问题,但它所带来对于开发效率的提高是史无前例的,它所带来对于服务开发及运维层面的成本削减也是肉眼可见的。你们可能不知道学了serverless的前端是什么概念,我们一般只会用两个字来形容这种人:全能!我经常说一句话,今天学serverless,明天就能开公司。今天看了这篇文章如果你还不能在Lambda上写serverless服务,我当场就把这个电脑屏幕吃掉。