乐趣区

关于腾讯云:用-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: cos
name: xart-oss
app: xart
stage: 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-<appid>.cos.ap-guangzhou.myqcloud.com,能够发现默认的命名规定是 https://&lt; 名字 -appid>.cos.< 地区 >.myqcloud.com

简略记录一下,在前面服务中会用到,遗记了也不要紧,看看 .envTENCENT_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.tsrequire("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/capi
SecretId=xxxx
SecretKey=xxxx

以上,server 局部就开发实现了,咱们能够通过在本地执行 node sls.js 来验证一下,应该能够看到服务启动的提醒了。

listen in http://localhost:8080

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

component: koa # 这里填写对应的 component
app: art
name: art-api
stage: 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: layer
app: art
name: art-api-layer
stage: dev

inputs:
  region: ap-guangzhou
  name: ${name}
  src: ../node_modules # 将 node_modules 打包上传
  runtimes:
    - Nodejs10.15 # 留神配置为雷同环境 

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

component: koa # 这里填写对应的 component
app: art
name: art-api
stage: 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";

export default function 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' 
}

// 略...
export default function 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: nextjs
app: art
name: art-front
stage: dev

inputs:
  src:
    dist: ./
    hook: npm run build
    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-utils
mkdir < 待挂载目标目录 >
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') };
};

最初

感激浏览,以上代码均通过实测,如果发现异常,那就再看一遍:)

有其余问题或想法,能够移步原文链接探讨。

源码:jiangqizheng/art,欢送 star。

One More Thing

立刻体验腾讯云 Serverless Demo,支付 Serverless 新用户礼包 ???? serverless/start

欢送拜访:Serverless 中文网!

退出移动版