这篇文章介绍了如何利用阿里云函数计算的多种独特功能来构建图片处理 Web 服务。
介绍函数计算
函数计算 是阿里云的 Serverless 计算平台,通过函数计算,工程师只需编写几行代码,即可开发互联网规模化服务。它可以无缝地进行资源管理、弹性伸缩和负载均衡,使开发人员可以专注于业务逻辑的开发,而不必费心管理底层基础架构,从而轻松构建可快速响应新信息的应用程序。在内部,我们利用容器技术并开发专有分布式算法,依托弹性扩展的资源编排用户的代码。我们已在内部开发了许多尖端技术,以求为我们的用户提供高可扩展性、可靠性和优异性能的服务。
在本指南中,我们将向您介绍展示其创新功能的分步式教程。如果这是您首次使用函数计算平台,您可以阅读此 快速入门指南 以熟悉基本的 Serverless 概念。
使用网络文件系统
我们推出的 访问 NAS 的功能,支持函数访问阿里云 文件存储服务。
益处
该平台的 Serverless 性质意味着每次调用时,用户代码可以在不同的实例上运行。这也意味着,函数不能依赖其本地文件系统来存储任何中间结果。开发人员必须依赖其他云服务,如 对象存储服务,在各函数或调用之间共享处理的结果。这不是理想做法,因为引入其他分布式服务将带来额外的开发成本,并增加代码的复杂性。
为了攻克此难题,我们开发了“访问文件存储 (NAS)”功能。NAS 是另一项阿里云服务,它提供高度可扩展、可靠且可用的分布式文件系统,并支持 标准文件访问协议。我们可以将远程 NAS 文件系统挂载到用户代码借以运行的资源上,从而有效地为函数代码创建一个“本地”文件系统。
图片爬虫示例
此演示部分介绍了如何创建 Serverless Web 爬虫程序,可以从种子网页下载所有图片。在 Serverless 平台上运行该程序是一个挑战,因为它不能在给定时间限制下,通过一个函数获取所有网站信息。但是,通过使用 NAS 功能,这一问题便迎刃而解,用户可以使用 NAS 文件系统在各次函数运行之间共享数据。下面我们介绍分步式教程。 我们假定您已理解 VPC 的概念,并知道如何在 VPC 中创建 NAS 挂载点。 如果您不了解这些内容,则可在继续以下步骤之前阅读 基础 NAS 教程。
使用 NAS 配置创建服务
1.登录 函数计算控制台。
2.选择您的 NAS 所在的目标地域。
3.创建使用预创建的 NAS 文件系统的服务。在本演示中:
1.输入服务名称和描述。
2.启用高级设置。
3.填写VPC 配置字段,确保您选择的 VPC 是 NAS 挂载点所在的 VPC。
完成VPC 配置后,会显示NAS 配置字段。
按如下示例,填写NAS 配置字段:
1.UserId和GroupId字段是函数在其下运行的 uid
/
gid。它们决定了在 NAS 文件系统上创建的所有文件的所有者。在此次演示中,您可以选择任何用户/设备组名,它们将在本服务中的所有函数间共享。
2.NAS 挂载点下拉菜单列出了可从所选 VPC 访问的所有有效 NAS 挂载点。
3.远程路径是 NAS 文件系统的一个目录,它不需要是 NAS 文件系统的根目录。请选择用于存储图片的目录。
4.本地挂载路径是函数可以访问远程目录的本地目录,请记住您在此处选择的内容。
5.使用 日志服务 为您所需的日志库目标配置日志。- 确保您配置了 角色 以授权函数计算平台访问 VPC 和日志库。
7.点击确定。
- 确保您配置了 角色 以授权函数计算平台访问 VPC 和日志库。
创建一个每五分钟启动一次的函数
我们已经创建了带有 NAS 访问的服务,现在可以编写爬虫程序了。 由于爬虫程序函数必须运行多次才能完成,因此我们使用 时间触发器 每 5 分钟调用一次。
1.登录到函数计算控制台,并选择您刚刚创建的服务
2.通过单击加号,为服务创建 函数。
3.函数计算平台提供了各种函数模板,可帮助您快速构建应用程序。选择为此演示创建一个空函数,然后单击“下一步”。但是如果您有时间,也可以试用其他模板。
4.在下一页的下拉菜单中选择时间触发器。填写触发器名称,将调用间隔设置为 5 分钟,将事件留空,然后单击“下一步”。
5.填写函数名称,并确保选择 java8 runtime。然后填写函数处理程序,将内存设置为 2048MB
,将“超时时间”设置为 300
秒,然后单击“下一步”
6.单击“下一步”并检查预览,然后单击“创建”。
在 Java 中写入爬虫程序
现在,您应该会看到函数代码页,可以写入爬虫程序了。处理程序逻辑非常简单,如下所示。
- 解析时间触发器事件以获取爬网程序配置。
- 根据配置创建图片爬网程序。爬网程序使用 JAVA HTML 解析器 解析 HTML 页,以识别图片和链接。
- 读取 NAS 文件系统中已被和尚未被访问的 Web 页面列表(仅当此函数在新环境中运行时)。
- 继续对 Web 页面的深度优先遍历,并使用爬虫程序下载这一过程中发现的任何新图片。
将新找到的 Web 页面保存到 NAS 文件系统。
下面是 Java 代码的摘录,您可以看到,我们以与本地文件系统相同的方式向 NAS 文件系统读取和写入文件。public class ImageCrawlerHandler implements PojoRequestHandler<TimedCrawlerConfig, CrawlingResult> { private String nextUrl() { String nextUrl; do { nextUrl = pagesToVisit.isEmpty() ? "" : pagesToVisit.remove(0); } while (pagesVisited.contains(nextUrl) ); return nextUrl; } private void initializePages(String rootDir) throws IOException { if (this.rootDir.equalsIgnoreCase(rootDir)) { return; } try { new BufferedReader(new FileReader(rootDir + CRAWL_HISTORY)).lines() .forEach(l -> pagesVisited.add(l)); new BufferedReader(new FileReader(rootDir + CRAWL_WORKITEM)).lines() .forEach(l -> pagesToVisit.add(l)); } catch (FileNotFoundException e) { logger.info(e.toString()); } this.rootDir = rootDir; } private void saveHistory(String rootDir, String justVistedPage, HashSet<String> newPages) throws IOException { //将爬网历史记录附加到文件末尾 try (PrintWriter pvfw = new PrintWriter( new BufferedWriter(new FileWriter(rootDir + CRAWL_HISTORY, true))); ) { pvfw.println(justVistedPage); } //将等待爬网的工作条目附加到文件末尾 try (PrintWriter ptfw = new PrintWriter( new BufferedWriter(new FileWriter(rootDir + CRAWL_WORKITEM, true))); ) { newPages.stream().forEach(p -> ptfw.println(p)); } } @Override public CrawlingResult handleRequest(TimedCrawlerConfig timedCrawlerConfig, Context context) { CrawlingResult crawlingResult = new CrawlingResult(); this.logger = context.getLogger(); CrawlerConfig crawlerConfig = null; try { crawlerConfig = JSON_MAPPER.readerFor(CrawlerConfig.class) .readValue(timedCrawlerConfig.payload); } catch (IOException e) { .... } ImageCrawler crawler = new ImageCrawler( crawlerConfig.rootDir, crawlerConfig.cutoffSize, crawlerConfig.debug, logger); int pagesCrawled = 0; try { initializePages(crawlerConfig.rootDir); if (pagesToVisit.isEmpty()) { pagesToVisit.add(crawlerConfig.url); } while (pagesCrawled < crawlerConfig.numberOfPages) { String currentUrl = nextUrl(); if (currentUrl.isEmpty()) { break; } HashSet<String> newPages = crawler.crawl(currentUrl); newPages.stream().forEach(p -> { if (!pagesVisited.contains(p)) { pagesToVisit.addAll(newPages); } }); pagesCrawled++; pagesVisited.add(currentUrl); saveHistory(crawlerConfig.rootDir, currentUrl, newPages); } //计算图片的总大小 ..... } catch (Exception e) { crawlingResult.errorStack = e.toString(); } crawlingResult.totalCrawlCount = pagesVisited.size(); return crawlingResult; } }
public class ImageCrawler { ... public HashSet<String> crawl(String url) { links.clear(); try { Connection connection = Jsoup.connect(url).userAgent(USER_AGENT); Document htmlDocument = connection.get(); Elements media = htmlDocument.select("[src]"); for (Element src : media) { if (src.tagName().equals("img")) { downloadImage(src.attr("abs:src")); } } Elements linksOnPage = htmlDocument.select("a[href]"); for (Element link : linksOnPage) { logDebug("Plan to crawl `" + link.absUrl("href") + "`"); this.links.add(link.absUrl("href")); } } catch (IOException ioe) { ... } return links; } }
为了简单起见,我们省略了一些细节和其他帮助类程序。如果您想要运行该代码,从您喜爱的网站上抓取图片,那么您可以从 awesome-fc github project 中获取完整代码。
运行爬虫程序
现在,代码已经编写好,可以运行了。步骤如下。
- 我们使用 maven 来执行依赖关系和版本管理。在与 Maven 库同步后(假定您已安装 Maven),只需键入以下命令,即可创建要上传的 jar 文件。
mvn clean package
- 在函数页面中选择“代码”选项卡。通过控制台上传在上一步中创建的 jar 文件(以依赖项的名称结尾)。
- 在函数页面中选择“触发器”选项卡。单击时间触发器链接,以 Json 格式输入事件。Json 事件将被序列化到爬网程序配置并传递到函数。单击“确定”。
- 时间触发器每五分钟调用一次爬虫程序功能。 每次调用时,处理程序将拾取仍需要访问的 URL 列表,并从第一个开始。
- 您可以选择“日志”选项卡来搜索爬虫程序执行日志。
创建 Serverless 服务
我们推出的第二项 功能 允许任何人发送 HTTP 请求,以直接触发函数执行。
益处
现在,我们已经有了装有从 Web 下载的图片的文件系统,我们希望找到一种方式,来通过 Web 服务提供这些图片。传统方法是将 NAS 挂载到虚拟机,然后在上面启动 Webserver。这既会浪费资源(如果该服务较少使用),也无法在数据流量大时扩展。作为替代方案,您可以编写一个 Serverless 函数,来读取存储在 NAS 文件系统上的图片,并通过 HTTP 访问域名提供图片。通过这种方式,您可以享受函数计算平台提供的即时可扩展性,同时仍只支付实际使用费用。
图片处理服务示例
此演示说明如何编写图片处理服务。
创建带有 HTTP 触发器的函数
1.登录到函数计算控制台,并选择与爬网程序函数相同的服务。
2.单击加号,为服务创建函数。
3.选择创建空的 python2.7 函数,然后单击“下一步”。
4.在下拉菜单中选择 HTTP 触发器,并确保它支持 GET
和 POST
调用方法,然后单击“下一步”。
5.完成该步骤的剩余部分,然后单击“确定”。
6.从同一 github 库中获取 文件,然后将目录上传到该函数。
使用 Python 处理图片
函数计算平台的 python 运行时附带了许多可供使用的内置模块。在本示例中,我们同时使用 [opencv] 和 wand 来执行图片转换。
使用 Python 中的 HTTP 触发器
即使对于图片处理函数,我们也需要设置网站以处理请求。一般情况下,需要使用另一种服务(如 API 网关)来处理 HTTP 请求。在本演示中,我们将使用函数计算平台的 HTTP 触发器 功能,允许 HTTP 请求直接触发函数执行。使用 HTTP 触发器,HTTP 请求中的报头/路径/查询会全部直接传递至函数处理程序,函数可以动态返回 HTML 内容。
基于这两种功能,处理程序代码非常简单,步骤如下。
- 从系统
environ
变量获取 HTTP 路径和查询。 - 使用 HTTP 路径加载 NAS 文件系统上的图片。
- 基于查询
action
应用不同的图片处理技术。 - 将转换后的图片插入预构建的 HTML 文件并返回。
下面是处理程序逻辑的摘录,我们可以看到,wand
像加载本地系统上的常规文件一样,加载存储在 NAS 上的图片。
import cv2
from wand.image import Image
TEMPLATE = open('/code/index.html').read()
NASROOT = '/mnt/crawler'
face_cascade = cv2.CascadeClassifier('/usr/share/opencv/lbpcascades/lbpcascade_frontalface.xml')
def handler(environ, start_response):
logger = logging.getLogger()
context = environ['fc.context']
path = environ.get('PATH_INFO', "/")
fileName = NASROOT + path
try:
query_string = environ['QUERY_STRING']
logger.info(query_string)
except (KeyError):
query_string = " "
action = query_dist['action']
if (action == "show"):
with Image(filename=fileName) as fc_img:
img_enc = base64.b64encode(fc_img.make_blob(format='png'))
elif (action == "facedetect"):
img = cv2.imread(fileName)
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
faces = face_cascade.detectMultiScale(gray, 1.03, 5)
for (x, y, w, h) in faces:
cv2.rectangle(img, (x, y), (x + w, y + h), (0, 0, 255), 1)
cv2.imwrite("/tmp/dst.png", img)
with open("/tmp/dst.png") as img_obj:
with Image(file=img_obj) as fc_img:
img_enc = base64.b64encode(fc_img.make_blob(format='png'))
elif (action == "rotate"):
assert len(queries) >= 2
angle = query_dist['angle']
logger.info("Rotate " + angle)
with Image(filename=fileName) as fc_img:
fc_img.rotate(float(angle))
img_enc = base64.b64encode(fc_img.make_blob(format='png'))
else:
# demo, mixed operation
status = '200 OK'
response_headers = [('Content-type', 'text/html')]
start_response(status, response_headers)
return [TEMPLATE.replace('{fc-py}', img_enc)]
后续步骤
现在我们已准备好函数和 HTTP 触发器,我们可以尝试 [图片旋转] 或像 [面部检测]之类的高级转换。
- URL 基于您的accountID/地域/服务/函数名构建,可参考 文章。
- 使用指向图片的本地 NAS 挂载目录的相对路径。您可以通过爬网程序日志找到 NAS 系统上的所有文件。
- 您可以直接在函数计算控制台上编辑 Python 代码,添加更多不同的图片转换程序并乐在其中。