写点什么

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

2021 年 3 月 07 日

如何用 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 年 3 月 07 日 23:231115

评论

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

15. Python 程序运行速度如何提高十倍?第一遍滚雪球学 Python 收工

梦想橡皮擦

Python 2月春节不断更

LeetCode题解:1143. 最长公共子序列,动态规划,JavaScript,详细注释

Lee Chen

算法 LeetCode 前端进阶训练营

日记 2021年2月19日(周五)

Changing Lin

2月春节不断更

诊所数字化:诊所医护人员绩效指标评估方式

boshi

绩效 数字化转型 医疗 七日更

翻译:《实用的Python编程》01_06_Files

codists

人工智能 后端 python 爬虫 数据结构与算法 文件操作

一、MongoDB简介

Kylin

数据库 mongodb 学习笔记 七日更 二月春节不断更

linux内核协议栈 邻居协议之ARP协议处理初始化

赖猫

Linux 协议栈 Linux内核

【STM32】ST-LINK下载器下载后需复位,程序才运行的问题

AXYZdong

硬件 stm32 2月春节不断更

CoralCache:一个提高微服务可用性的中间件

华为云开发者社区

数据库 微服务 中间件 内存 CoralCache

技术需求文档,应当这么写!

Python编程参考官方账号

需求 文档

TDD测试驱动开发的实践心得

御剑

架构 TDD 测试驱动开发

竞猜商城系统软件制作

v16629866266

让虞书欣、李诞拍到停不下来!AR+AI双引擎的互动小游戏,如何打开IP新玩法?

爱奇艺技术产品团队

如何读懂CNN、BBC、经济学人、卫报、纽约时报?看完这本经典即可事半功倍!

wbliu85

英语 学习经验

前端学习总结,经验分享,项目经验分享过程

魔王哪吒

学习 程序员 Vue 前端 2月春节不断更

5.state更新流程(setState里到底发生了什么)

全栈潇晨

React React Hooks react源码

最新Hadoop的面试题总结

大数据老哥

TCP 协议灵魂问题,巩固你的网路底层基础

Hive HMS Canary 时间较长异常分析

笨小康

大数据 hadoop hive

6.render阶段(厉害了,我有创建Fiber的技能)

全栈潇晨

React React Hooks react源码

如何提升网页核心指标

Vincent

Elasticsearch Search API 基础语法

escray

elastic 七日更 死磕Elasticsearch 60天通过Elastic认证考试 2月春节不断更

DPDK大页内存原理

赖猫

Linux DPDK

GitHub上爆火的Java性能优化100+小技巧!(干货建议收藏)

Java架构师迁哥

地表建筑物识别Dayo1

Tango

七日更 2月春节不断更

对DevOps的九大误解,是时候纠正了!

禅道项目管理

开源 DevOps 敏捷 自动化 持续交付

【LeetCode】K 连续位的最小翻转次数Java题解

HQ数字卡

算法 LeetCode 2月春节不断更

电信的标准化组织

Geek_古藤模根

标准化 电信

使用 Tye 辅助开发 k8s 应用竟如此简单(三)

newbe36524

Docker 微服务 k8s dotnet

元旦立下的Flag,春节后该如何实现?

脑极体

全程干货无废话!腾讯强推Redis深度笔记我粉了!

Java王路飞

Java 数据库 redis 程序员 面试

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