写点什么

如何用 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:231855

评论

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

我在极客大学算法训练营的收获

熊斌

极客时间 极客大学

Java并发编程系列——分布式锁

孙苏勇

Java zookeeper 并发编程 多线程 分布式锁

创投机会诞生在这四个核心变量中 | 2019年在某大学课堂做的一次讲演的实录

邓瑞恒Ryan

创业 管理 投资 行业资讯

五十年前的一桩公案:数据库关系模型的流行史(下)

青菜年糕汤

数据库 分布式数据库 数据库规范 关系型数据库 数据库设计

笔记:《如何系统思考》之因果回路图

wiflish

思维方式

人人都要有经营意识

Neco.W

创业 重新理解创业 公司管理

实战营第一战:FizzBuzz

escray

学习 CSD 认证实战营

python中的GIL锁和互斥锁问题

半面人

Python

一文带你搞懂RPC核心原理

松花皮蛋me

微服务 RPC 远程调用

中台是为了复用?未必!浅谈产业中台建设的特点与误区

孤岛旭日

架构 中台 企业中台 企业架构 产业互联网

五十年前的一桩公案:数据库关系模型的流行史(上)

青菜年糕汤

数据库 分布式数据库 数据库规范 关系型数据库 数据库设计

Impala UDTF 功能实现

小鹏

大数据 hadoop cloudera 数据仓库

如何成为一个高效的问题解决者?

汪锋

Golang杂谈 - graceful shutdown为何离奇失效?

星语

后端 平滑重启 服务端 Go 语言

leetcode141. 环形链表

Damien

算法 链表 LeetCode

leetcode8. 字符串转换整数 (atoi)

Damien

算法 数学

基准测试神器JMH —— 详解36个官方例子

捉虫大师

Java 性能 JMH

聊天机器人为什么这么难?

青菜年糕汤

人工智能 自然语言处理 搜索引擎 chatbot 聊天机器人

为什么厉害的人精力都那么好?

非著名程序员

程序员 程序人生 提升认知 精力管理

Redis学习笔记(安装)

编程随想曲

redis

游戏夜读 | 工具游戏的辉煌

game1night

哲少荐书:这才是心理学

Jackey

心理学 读书

python oop 指南

志学Python

Python python 爬虫 oop

Web3极客日报#135

谢锐 | Frozen

区块链 独立开发者 技术社区 Rebase Web3 Daily

NIO看破也说破(一)—— Linux/IO基础

小眼睛聊技术

Linux 架构 后端 Netty nio

MySQL自增ID以及其他唯一ID方式分析

Bruce Duan

MySQL自增ID 唯一ID

Web3极客日报#134

谢锐 | Frozen

区块链 独立开发者 技术社区 Rebase Web3 Daily

冥想与呼吸法之于情绪控制

树上

情绪 冥想 呼吸法 呼吸 自我

写在2020年五四青年节

耿老的竹林

个人成长

花更多的时间在自己的优势上

Neco.W

创业 自我管理 重新理解创业

Java并发编程基础--线程

Java收录阁

Java 线程

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