写点什么

如何用 Serverless 优雅地实现图片艺术化应用

  • 2021-03-07
  • 本文字数:8740 字

    阅读完需:约 29 分钟

如何用 Serverless 优雅地实现图片艺术化应用

本文将分享如何从零开始搭建一个基于腾讯云 Serverless 的图片艺术化应用!

项目已开源,完整代码见文末

线上 demo 预览: https://art.x96.xyz/

在完整阅读文章后,读者应该能够实现并部署一个相同的应用,这也是本篇文章的目标。


项目看点概览:


  • 前端 react(Next.js)、后端 node(koa2)

  • 全面使用 ts 进行开发,极致开发体验(后端运行时 ts 的方案,虽然性能差点,不过胜在无需编译,适合写 demo)

  • 突破云函数代码 500mb 限制(提供解决方案)

  • TensorFlow2 + Serverless 扩展想象力边际

  • 高性能,轻松应对万级高并发,实现高可用(自信的表情,反正是平台干的活)

  • 秒级部署,十秒部署上线

  • 开发周期短(本文就能带你完成开发)



本项目部署借助了 Serverless component,因此当前开发环境需先全局安装 Serverless 命令行工具


npm install -g serverless
复制代码


需求与架构


本应用的整体需求很简单:图片上传与展示。


  1. 模块概览

模块概览

  1. 上传图片

上传图片

  1. 浏览图片

浏览图片


用对象存储提供存储服务


在开发之前,我们先创建一个 oss 用于提供图片存储(可以使用你已有的对象存储)


mkdir oss
复制代码

在新建的 oss 目录下添加 serverless.yml

component:cosname:xart-ossapp:xartstage:dev
inputs: src: src:./ exclude: -.env# 防止密钥被上传 bucket:${name}# 存储桶名称,如若不添加 AppId 后缀,则系统会自动添加,后缀为大写(xart-oss-<你的appid>) website:false targetDir:/ protocol:https region:ap-guangzhou# 配置区域,尽量配置在和服务同区域内,速度更快 acl: permissions:public-read# 读写配置为,私有写,共有读
复制代码

执行 sls deploy 几秒后,你应该就能看到如下提示,表示新建对象存储成功。

新建对象存储

这里,我们看到 url https://art-oss-.cos.ap-guangzhou.myqcloud.com,可以发现默认的命名规则是 https://<名字-appid>.cos.<地域>.myqcloud.com

简单记录一下,在后面服务中会用到,忘记了也不要紧,看看 .env 内 TENCENT_APP_ID 字段(部署后会自动生成 .env)


实现后端服务


新建一个目录并初始化

mkdir art-api && cd art-api && npm init
复制代码

安装依赖(期望获取 ts 类型提示,请自行安装 @types)

npm i koa @koa/router @koa/cors koa-body typescript ts-node cos-nodejs-sdk-v5 axios dotenv
复制代码

配置 tsconfig.json

{  "compilerOptions": {    "target": "es2018",    "module": "commonjs",    "lib": ["es2018", "esnext.asynciterable"],    "experimentalDecorators": true,    "emitDecoratorMetadata": true,    "esModuleInterop": true  }}
复制代码

入口文件 sls.js

require("ts-node").register({ transpileOnly: true }); // 载入 ts 运行时环境,配置忽略类型错误module.exports = require("./app.ts"); // 直接引入业务逻辑,下面我会和你一起实现
复制代码

补充两个实用知识点:

node -r

在入口文件中引入 require("ts-node").register({ transpileOnly: true }) 实际等同于 node -r ts-node/register/transpile-only

所以 node -r 就是在执行之前载入一些特定模块,利用这个能力,能快速实现对一些功能的支持

比如 node -r esm main.js 通过 esm 模块就能在无需 babel、webpack 的情况下快速 import 与 export 进行模块加载与导出

ts 加载路径

如果不希望用 ../../../../../ 来加载模块,那么

  1. 在 tsconfig.json 中配置 baseUrl: "."

  2. ts-node -r tsconfig-paths/register main.ts 或 require("tsconfig-paths").register()

  3. import utils from 'src/utils' 即可愉快地从项目根路径加载模块

下面来实现具体逻辑:

app.ts

require("dotenv").config(); // 载入 .env 环境变量,可以将一些密钥配置在环境变量中,并通过 .gitignore 阻止提交import Koa from"koa";import Router from"@koa/router";import koaBody from"koa-body";import cors from'@koa/cors'import util from'util'import COS from'cos-nodejs-sdk-v5'import axios from'axios'
const app = new Koa();const router = new Router();
var cos = new COS({ SecretId: process.env.SecretId // 你的id, SecretKey: process.env.SecretKey // 你的key,});
const cosInfo = { Bucket: "xart-oss-<你的appid>", // 部署oss后获取 Region: "ap-guangzhou",}
const putObjectSync = util.promisify(cos.putObject.bind(cos));const getBucketSync = util.promisify(cos.getBucket.bind(cos));
router.get("/hello", async (ctx) => { ctx.body = 'hello world!'})
router.get("/api/images", async (ctx) => { const files = await getBucketSync({ ...cosInfo, Prefix: "result", });
const cosURL = `https://${cosInfo.Bucket}.cos.${cosInfo.Region}.myqcloud.com`; ctx.body = files.Contents.map((it) => { const [timestamp, size] = it.Key.split(".jpg")[0].split("__"); const [width, height] = size.split("_"); return { url: `${cosURL}/${it.Key}`, width, height, timestamp: Number(timestamp), name: it.Key, }; }) .filter(Boolean) .sort((a, b) => b.timestamp - a.timestamp);});
router.post("/api/images/upload", async (ctx) => { const { imgBase64, style } = JSON.parse(ctx.request.body) const buf = Buffer.from(imgBase64.replace(/^data:image\/\w+;base64,/, ""), 'base64') // 调用预先提供tensorflow服务加工图片,后面替换成你自己的服务 const { data } = await axios.post('https://service-edtflvxk-1254074572.gz.apigw.tencentcs.com/release/', { imgBase64: buf.toString('base64'), style }) if (data.success) { const afterImg = await putObjectSync({ ...cosInfo, Key: `result/${Date.now()}__400_200.jpg`, Body: Buffer.from(data.data, 'base64'), }); ctx.body = { success: true, data: 'https://' + afterImg.Location } }});
app.use(cors());app.use(koaBody({ formLimit: "10mb", jsonLimit: '10mb', textLimit: "10mb"}));app.use(router.routes()).use(router.allowedMethods());
const port = 8080;app.listen(port, () => { console.log("listen in http://localhost:%s", port);});
module.exports = app;
复制代码

在代码里可以看到,在图片上传采用了 base64 的形式。这里需要注意,通过 api 网关触发 scf 的时候,网关无法透传 binary,具体上传规则可以参阅官方文档:

再补充一个知识点:实际我们访问的是 api 网关,然后触发云函数,来获得请求返回结果,所以 debug 时需要关注全链路

回归正题,接着配置环境变量 .env

NODE_ENV=development
# 配置 oss 上传所需密钥,需要自行配置,配好了也别告诉我:)# 密钥查看地址:https://console.cloud.tencent.com/cam/capiSecretId=xxxxSecretKey=xxxx
复制代码

以上,server 部分就开发完成了,我们可以通过在本地执行 node sls.js 来验证一下,应该可以看到服务启动的提示了。

listen in http://localhost:8080

来简单配置一下 serverless.yml,把服务部署到线上,后面再进一步使用 layer 进行优化

component:koa# 这里填写对应的 componentapp:artname:art-apistage:dev
inputs: src: src:./ exclude: -.env functionName:${name} region:ap-guangzhou runtime:Nodejs10.15 functionConf: timeout:60# 超时时间配置的稍微久一点 environment: variables:# 配置环境变量,同时也可以直接在scf控制台配置 NODE_ENV:production apigatewayConf: enableCORS:true protocols: -https -http environment:release
复制代码

之后执行部署命令 sls deploy

等待数十秒,应该会得到如下的输出结果(如果是第一次执行,需要平台方授权)

其中 url 就是当前服务部署在线上的地址,我们可以试着访问一下看看,是否看到了预设的 hello world。

到这里,server 基本上已经部署完成了。如果代码有改动,那就修改后再次执行 sls deploy。官方为代码小于 10M 的项目提供了在线编辑的能力。

但是,随着项目复杂度的增加,deploy 上传会变慢。所以,让我们再优化一下。

新建 layer 目录

mkdir layer
复制代码

在 layer 目录下添加 serverless.yml

component:layerapp:artname:art-api-layerstage:dev
inputs: region:ap-guangzhou name:${name} src:../node_modules# 将 node_modules 打包上传 runtimes: -Nodejs10.15# 注意配置为相同环境
复制代码

回到项目根目录,调整一下根目录的 serverless.yml

component:koa# 这里填写对应的 componentapp:artname:art-apistage:dev
inputs: src: src:./ exclude: -.env -node_modules/**# deploy 时排除 node_modules functionName:${name} region:ap-guangzhou runtime:Nodejs10.15 functionConf: timeout:60# 超时时间配置的稍微久一点 environment: variables:# 配置环境变量,同时也可以直接在 scf 控制台配置 NODE_ENV:production apigatewayConf: enableCORS:true protocols: -https -http environment:release layers: -name:${output:${stage}:${app}:${name}-layer.name}# 配置对应的 layer version:${output:${stage}:${app}:${name}-layer.version}# 配置对应的 layer 版本
复制代码

接着执行命令 sls deploy --target=./layer 部署 layer,然后这次部署看看速度应该已经在 10s 左右了

sls deploy
复制代码

关于 layer 和云函数,补充两个知识点:

layer 的加载与访问

layer 会在函数运行时,将内容解压到 /opt 目录下,如果存在多个 layer,那么会按时间循序进行解压。如果需要访问 layer 内的文件,可以直接通过 /opt/xxx 访问。如果是访问 node_module 则可以直接 import,因为 scf 的 NODE_PATH 环境变量默认已包含 /opt/node_modules 路径。

配额

云函数 scf 针对每个用户帐号,均有一定的配额限制:

其中需要重点关注的就是单个函数代码体积 500mb 的上限。在实际操作中,云函数虽然提供了 500mb。但也存在着一个 deploy 解压上限。


关于绕过配额问题:

  • 如果超的不多,那么使用 npm install --production 就能解决问题

  • 如果超的太多,那就通过挂载 cfs 文件系统来进行规避,我会在下面部署 tensorflow 算法模型服务章节里面,展开聊聊如何把 800mb tensorflow 的包 + 模型部署到 SCF 上


实现前端 SSR 服务


下面将使用 next.js 来构建一个前端 SSR 服务。

新建目录并初始化项目:

mkdir art-front && cd art-front && npm init
复制代码

安装依赖:

npm install next react react-dom typescript @types/node swr antd @ant-design/icons dayjs
复制代码

增加 ts 支持(next.js 跑起来会自动配置):

touch tsconfig.json
复制代码

打开 package.json 文件并添加 scripts 配置段:

"scripts": {  "dev": "next",  "build": "next build",  "start": "next start"}
复制代码

编写前端业务逻辑(文中仅展示主要逻辑,源码在 GitHub 获取)

pages/_app.tsx

import React from"react";import"antd/dist/antd.css";import { SWRConfig } from"swr";
exportdefaultfunction MyApp({ Component, pageProps }) { return ( <SWRConfig value={{ refreshInterval: 2000, fetcher: (...args) => fetch(args[0], args[1]).then((res) => res.json()), }} > <Component {...pageProps} /> </SWRConfig> );}
复制代码

pages/index.tsx  完整代码

import React from"react";import { Card, Upload, message, Radio, Spin, Divider } from"antd";import { InboxOutlined } from"@ant-design/icons";import dayjs from"dayjs";import useSWR from"swr";
let origin = 'http://localhost:8080'if (process.env.NODE_ENV === 'production') { // 使用你自己的部署的art-api服务地址 origin = 'https://service-5yyo7qco-1254074572.gz.apigw.tencentcs.com/release'}
// 略...exportdefaultfunction Index() { const { data } = useSWR(`${origin}/api/images`);
const [img, setImg] = React.useState(""); const [loading, setLoading] = React.useState(false);
const uploadImg = React.useCallback((file, style) => { const reader = new FileReader(); reader.readAsDataURL(file); reader.onload = async () => { const res = await fetch( `${origin}/api/images/upload`, { method: 'POST', body: JSON.stringify({ imgBase64: reader.result, style }), mode: 'cors' } ).then((res) => res.json());
if (res.success) { setImg(res.data); } else { message.error(res.message); } setLoading(false); } }, []);
const [artStyle, setStyle] = React.useState(STYLE_MODE.cube);
return ( <Dragger style={{ padding: 24 }} {...{ name: "art_img", showUploadList: false, action: `${origin}/api/upload`, onChange: (info) => { const { status } = info.file; if (status !== "uploading") { console.log(info.file, info.fileList); } if (status === "done") { setImg(info.file.response); message.success(`${info.file.name} 上传成功`); setLoading(false); } else if (status === "error") { message.error(`${info.file.name} 上传失败`); setLoading(false); } }, beforeUpload: (file) => { if ( !["image/png", "image/jpg", "image/jpeg"].includes(file.type) ) { message.error("图片格式必须是 png、jpg、jpeg"); return false; } const isLt10M = file.size / 1024 / 1024 < 10; if (!isLt10M) { message.error("文件大小超过10M"); return false; } setLoading(true);
uploadImg(file, artStyle); return false; }, }} // 略...
复制代码

使用 npm run dev 把前端跑起来看看,看到以下提示就是成功了


ready - started server on http://localhost:3000


接着配置 serverless.yml(如果有需要可以参考前文,使用 layer 优化部署体验)

component:nextjsapp:artname:art-frontstage:dev
inputs: src: dist:./ hook:npmrunbuild exclude: -.env region:ap-guangzhou functionName:${name} runtime:Nodejs12.16 staticConf: cosConf: bucket:art-front# 将前端静态资源部署到oss,减少scf的调用频次 apigatewayConf: enableCORS:true protocols: -https -http environment:release # customDomains: # 如果需要,可以自己配置自定义域名 # - domain: xxxxx # certificateId: xxxxx # 证书 ID # # 这里将 API 网关的 release 环境映射到根路径 # isDefaultMapping: false # pathMappingSet: # - path: / # environment: release # protocols: # - https functionConf: timeout:60 memorySize:128 environment: variables: apiUrl:${output:${stage}:${app}:art-api.apigw.url}# 此处可以将api通过环境变量注入
复制代码

由于我们额外配置了 oss,所以需要额外配置一下 next.config.js

const isProd = process.env.NODE_ENV === "production";
const STATIC_URL = "https://art-front-<你的appid>.cos.ap-guangzhou.myqcloud.com/";
module.exports = { assetPrefix: isProd ? STATIC_URL : "",};
复制代码


提供 Tensorflow 2.x 算法模型服务


在上面的例子中,我们使用的 Tensorflow,暂时还是调用我预先提供的接口。

接着让我们会把它替换成我们自己的服务。

基础信息

  • tensoflow2.3

  • model

scf 在 python 环境下,默认提供了 tensorflow1.9 依赖包,使用 python 可以用较低的成本直接上手。

问题所在

但如果你想使用 2.x 版本,或不熟悉 python,想用 node 来跑 tensorflow,那么就会遇到代码包大小的限制的问题。

  • Python 中 Tensorflow 2.3 包体积 800mb 左右

  • node 中 tfjs-node2.3 安装后,同样会超过 400mb(tfjs core 版本,非常小,不过速度太慢)

怎么解决 —— 文件存储服务!

先看看 CFS 文档的介绍

挂载后,就可以正常使用了,腾讯云提供了一个简单例子。

var fs = requiret('fs');exports.main_handler = async (event, context) => {  await fs.promises.writeFile('/mnt/myfolder/filel.txt', JSON.stringify(event));   return event;};
复制代码

既然能正常读写,那么就能够正常的载入 npm 包,可以看到我直接加载了 /mnt 目录下的包,同时 model 也放在 /mnt 下

  tf = require("/mnt/nodelib/node_modules/@tensorflow/tfjs-node");  jpeg = require("/mnt/nodelib/node_modules/jpeg-js");  images = require("/mnt/nodelib/node_modules/images");  loadModel = async () => tf.node.loadSavedModel("/mnt/model");
复制代码

如果你使用 Python,那么可能会遇到一个问题,那就是 scf 默认环境下提供了 tensorflow 1.9 的依赖包,所以需要使用 insert,提高 /mnt 目录下包的优先级

sys.path.insert(0, "./mnt/xxx")
复制代码

上面提供了解决方案,那么具体开发中可能会感觉很麻烦,因为 csf 必须和 scf 配置在同一个子网内,无法挂载到本地进行操作。

所以,在实际部署过程中,可以在对应网络下,购置一台按需计费的 ecs 云服务器实例。然后将硬盘挂载后,直接进行操作,最后在云函数成功部署后,销毁实例:)

sudo yum install nfs-utilsmkdir <待挂载目标目录>sudo mount -t nfs -o vers=4.0,noresvport <挂载点IP>:/ <待挂载目录>
复制代码


具体业务代码如下:

const fs = require("fs");let tf, jpeg, loadModel, images;
if (process.env.NODE_ENV !== "production") { tf = require("@tensorflow/tfjs-node"); jpeg = require("jpeg-js"); images = require("images"); loadModel = async () => tf.node.loadSavedModel("./model");} else { tf = require("/mnt/nodelib/node_modules/@tensorflow/tfjs-node"); jpeg = require("/mnt/nodelib/node_modules/jpeg-js"); images = require("/mnt/nodelib/node_modules/images"); loadModel = async () => tf.node.loadSavedModel("/mnt/model");}
exports.main_handler = async (event) => { const { imgBase64, style } = JSON.parse(event.body) if (!imgBase64 || !style) { return { success: false, message: "需要提供完整的参数imgBase64、style" }; } time = Date.now(); console.log("解析图片--"); const styleImg = tf.node.decodeJpeg(fs.readFileSync(`./imgs/style_${style}.jpeg`)); const contentImg = tf.node.decodeJpeg( images(Buffer.from(imgBase64, 'base64')).size(400).encode("jpg", { operation: 50 }) // 压缩图片尺寸 ); const a = styleImg.toFloat().div(tf.scalar(255)).expandDims(); const b = contentImg.toFloat().div(tf.scalar(255)).expandDims(); console.log("--解析图片 %s ms", Date.now() - time);

time = Date.now(); console.log("载入模型--"); const model = await loadModel(); console.log("--载入模型 %s ms", Date.now() - time);

time = Date.now(); console.log("执行模型--"); const stylized = tf.tidy(() => { const x = model.predict([b, a])[0]; return x.squeeze(); }); console.log("--执行模型 %s ms", Date.now() - time);
time = Date.now();
const imgData = await tf.browser.toPixels(stylized); var rawImageData = { data: Buffer.from(imgData), width: stylized.shape[1], height: stylized.shape[0], };
const result = images(jpeg.encode(rawImageData, 50).data) .draw( images("./imgs/logo.png"), Math.random() * rawImageData.width * 0.9, Math.random() * rawImageData.height * 0.9 ) .encode("jpg", { operation: 50 });
return { success: true, data: result.toString('base64') };};
复制代码


最后


感谢阅读,以上代码均经过实测,如果发现异常,那就再看一遍:)



头图:Unsplash

作者:蒋启钲

原文:https://mp.weixin.qq.com/s/PM1Y3P2XZ341l56eaneKGA

原文:如何用 Serverless 优雅地实现图片艺术化应用

来源:TencentServerless - 微信公众号 [ID:ServerlessGo]

转载:著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

2021-03-07 23:232000

评论

发布
暂无评论
发现更多内容

抖音严打虚假宣传滋补膳食内容广告主:必须严格监管信息流广告

石头IT视角

八、高可用之故障隔离

穿过生命散发芬芳

5月月更 高可用设计

java内存模型之happenbefore原则

急需上岸的小谢

5月月更

目标检测的算法

恒山其若陋兮

5月月更

模块五:作业

本人法海

「架构实战营」

百万级电商秒杀架构设计

晨亮

「架构实战营」

【建议收藏】Mysql知识干货(mysql八股文)汇总

利志分享

面试 面试题 MySQL 数据库 面试问题 MySQL InnoDB

架构实战营总结

晨亮

「架构实战营」

Bigdata 作业第九周

Pyel

docker可视化管理工具之shipyard

乌龟哥哥

5月月更

【愚公系列】2022年05月 二十三种设计模式(八)-组合模式(Composite Pattern)

愚公搬代码

5月月更

架构训练营-毕业总结

默光

架构训练营5期

Go Web 编程入门:创建动态 HTML 和文本文件

宇宙之一粟

Web Go 语言 5月月更

复杂任务中,流程的解耦设计

架构 事件驱动 任务管理 异步设计

从这些云原生企业身上,我看到了数字化创新者该有的样子

阿里巴巴云原生

阿里云 云原生 实战案例

python使用 pywin32 模块操作 excel,Python 操作 excel 系列之五

梦想橡皮擦

5月月更

Nginx 的日志

HoneyMoose

JAVA为什么需要泛型?

源字节1号

软件开发 后端开发 小程序开发

在线HTML文本提取URL链接工具

入门小站

工具

Nacos源码系列—服务端那些事儿

牧小农

源码 nacos

面向对象的系统分析

奔向架构师

信息系统 5月月更

浅谈Java中的Math.random

工程师日月

java 5月月更

函数计算 HTTP 触发器支持异步,解放双手搭建 Web 服务

阿里巴巴云原生

阿里云 云原生 函数计算

集成 ShenYu 网关实现 Dubbo 泛化调用

码农大熊

盘古开发框架 Dubbo网关 泛化调用 ShenYu网关

模块五作业

HZ

架构实战营 #架构实战营

linux之man命令

入门小站

Hadoop Java api操作hdfs(一)

Emperor_LawD

hadoop 5月月更

赫卡忒的眼眸:微光手机“夜视仪”是怎样炼成的?

脑极体

在线TSV转HTMLTable工具

入门小站

工具

机器学习:真正的底层是什么?

海拥(haiyong.site)

5月月更

OpenYurt 开源之夏开始申请啦

阿里巴巴云原生

阿里云 云原生 开源之夏

如何用 Serverless 优雅地实现图片艺术化应用_服务革新_TencentServerless_InfoQ精选文章