三月份的时候收到阿里云的促销邮件,内容是关于他们的函数计算服务的,从中我了解到阿里云的函数计算可以使用自定义runtime来实现函数;即只要能够监听端口提供服务的内容都可以函数化,当时我就开了个脑洞,能不能把博客也函数化,这样uptime有保障,也不用再单独弄台服务器来跑服务,降低了成本还省心。

为此我去做了点功课,并进行了一番尝试,成功将博客搬了上去,虽然博客的响应速度下降了,不过本来就是没什么人看也就无所谓了(,最近终于有点空,来整理一下函数化的笔记。

先挖个坑,慢慢更……

序:认识函数

什么是函数计算?

阿里云的网站上是这么说的:函数计算是事件驱动的全托管计算服务,无需采购与管理服务器等基础设施,只需编写并上传代码,函数计算就为您准备好计算资源,并弹性地、可靠地运行任务,同时提供日志查询、性能监控和报警等功能。

举个例子来说,如果我们需要从零开始实现一个网络服务,一般来说是先去购买一台VPS,然后根据服务的需求编写一个server,并让这个server去监听某个端口,将该端口暴露到公网然后提供服务。这其中就会有很多问题,首先,VPS的供应商是否可靠,网络是否健壮,会不会偶尔宕机、断网?其次,如果你配置server的时候,犯了低级错误(例如ssh弱口令,没有改默认端口,暴露了危险服务等等),会不会留下安全隐患等等……最后,购买VPS需要多高的配置,如何在性能和成本间做好平衡?

函数计算的出现解决了一些痛点,首先,管理者不再需要去关心VPS本身的可靠性,函数计算是动态部署的而不是局限于某台物理机器,你几乎永远遇不上宕机和网络问题;其次,函数计算只暴露网络服务,并且函数本身也是运行在类似docker的隔离、虚拟环境中的,通过比较合理地安排资源,安全性非常高,几乎不用担心受到攻击(除非你网络服务出了大漏洞爆个shell或者能让别人直接操作你数据库然后删库跑路);最后,函数计算能够弹性伸缩,不用担心买的配置不够,而且函数的计费是按照运算时长×资源大小收费的(数据库、网络、存储等周围服务当然是另外计费的),不用不收费,对于中低负载的应用来说这种计费方式几乎就代表了最低的成本。

你说得这么好,要怎么用呢?

以golang举个例子吧(虽然阿里云不支持直接用golang写函数):

func handler(w http.ResponseWriter, req *http.Request) {
    // 处理req请求
    // 将response写到w里
    // return
}

你只需要实现一个类似的handler函数,把对应代码贴上去,选择运算资源大小(最大内存)并发布,你的服务就跑起来了。

??????就这?

当然,说起来是很简单,实际上函数写起来还是很麻烦的,首先,上述这种最简单的方式需要服务商提供支持,比如阿里云目前只支持使用Node.js、Python、PHP、Java、C#直接进行开发,当然,上述函数也不是随便写的,你要参考函数计算的文档来了解req中会有哪些重要的参数,需要以什么格式生成response等等,如何同周围服务进行交互(比如函数需要访问云数据库等等)同样也需要查阅文档。如果你顺手的语言不包含这几种,或者你嫌弃Python的运行效率觉得它会harm你的成本,那么阿里云还提供了CustomRuntime的方式,这也是我使用的方式;这种方式提供了最大的灵活性,你只需要提供一个脚本或者二进制,在运行后能有应用监听9000端口并提供服务,即可作为一个函数提供服务。

阿里云的CustomRuntime

阿里云的CustomRuntime逻辑大致如下:

  • 首先你需要准备一个压缩包,压缩包内包含自定义环境所需的全部内容(包括动态链接库等);
  • 当一个新的函数”容器“被创建的时候,相当于将你准备的压缩包内的内容解压到了一个Debian9系统的/code目录下,然后启动器会以root用户的身份运行/code/bootstrap文件,这里的bootstrap需要有x权限,可以是一段bash脚本,也可以是二进制文件;启动器期望在运行bootstrap后,在0.0.0.0:9000端口或者0.0.0.0:${FC_SERVER_PORT}有服务监听,这之后函数计算将会把所有的请求转发到该端口;
  • 虽然以root身份运行,但程序并没有文件系统的读写权限,只能读写/tmp目录,并且限制总大小不超过512MB;
  • 程序运行时的内存占用不能超过创建函数时设置的大小,否则容器会被kill

我们可以发现,虽然我们可以放入任意的可执行程序作为bootstrap并提供服务,但是大部分程序都有动态链接库的依赖,而对应的运行环境中可能并没有对应依赖;对于该问题,我们可以提前把动态链接库打包,并使用环境变量提醒新的连接位置,或者使用funcraft来打包环境,这个我们后面再提。

函数化博客程序

程序选择

常见的博客程序有typechowordpress,后者虽然功能强大但是同样占用资源也较多,另外有一些静态博客程序例如hexo,本身可以依赖GitHub Pages来进行部署,不需要如此大费周折。在这里我选择typecho,一个是上手容易,另外一个是占用资源极低,从我的实践来看,使用typecho并使用sqlite3作为数据库,运行时内存占用一般不会超过90MB,为了保险起见,我的容器内存大小选择的是192MB,完全够用。

配套Server程序

首先PHP肯定是跑不掉的,来个PHP-FPM吧,web服务器的话可以选择nginx,不过由于最初尝试的时候走了弯路,我最后选择的是caddy,这玩意儿是golang编写的,应付中低负载完全足够,而且Go语言编译得到的二进制几乎没有外部依赖,可以放心丢上去。

梳理

梳理一下请求路径:

请求 -> CDN -> CDN缓存/回源函数caddy                -> NAS存储博客本体/数据库
           -> 动态部分回源函数 -> caddy -> php-fpm  ↑

其中NAS将被挂载到函数容器上(对应操作可以通过funcraft或者控制台完成)作为一个可读写的块设备

Funcraft

错误的尝试

最初的时候,我尝试静态编译nginxphp-fpm,或者将它们的动态链接库打包和程序一起上传,经历了很多麻烦且未成功,直到我发现了阿里云自己的Funcraft工具,让你的CustomRuntime环境搭建变得超级方便

安装

请按照GitHub上的指示安装,并配置好区域和ACCESS ID/SECRET等

目录结构

你可以将任意一个目录变成你的函数环境,结构如下:

 |
 | - .fun
 |     | - nas
 |     |    | - blabla
 |     |
 |     | - build
 |          | - blabla
 |
 | - bootstrap
 | - template.yml
 | - Funfile
 | - something else

其中.fun目录是通过fun nasfun build等命令自动生成的,你可以使用fun nas创建一个新的nas,或者将已有的nas与之关联,并将对应目录里的内容增量同步到阿里云NAS服务中;fun build将根据Funfile内的内容安装你需要的依赖并与标准镜像进行差分,替你管理好函数的依赖并打包成函数。template.yml则包含了函数的全部配置信息,包括容器大小、NAS的挂载点、自定义函数地址等等。

Funfile

这里给出一个示例,关于如何使用Funfile直接安装php-fpm环境:

RUNTIME custom
RUN apt-get update
RUN apt-get -y install apt-transport-https lsb-release ca-certificates wget
RUN wget -O /etc/apt/trusted.gpg.d/php.gpg https://packages.sury.org/php/apt.gpg
RUN sh -c 'echo "deb https://packages.sury.org/php/ $(lsb_release -sc) main" > /etc/apt/sources.list.d/php.list'
RUN apt-get update

RUN fun-install apt-get install php7.2-fpm
RUN fun-install apt-get install php7.2-common
RUN fun-install apt-get install php7.2-gd
RUN fun-install apt-get install php7.2-json
RUN fun-install apt-get install php7.2-mbstring
RUN fun-install apt-get install php7.2-mysql
RUN fun-install apt-get install php7.2-opcache
RUN fun-install apt-get install php7.2-soap
RUN fun-install apt-get install php7.2-sqlite3
RUN fun-install apt-get install php7.2-xml 
RUN fun-install apt-get install php7.2-zip

语法非常简单,几乎就是直接RUN fun-install加上bash命令,通过fun build之后,对应的环境将以差分的形式作为函数的一部分,并在之后被打包上传。

template.yml

给出一个template的示例,几乎不需要解释

ROSTemplateFormatVersion: '2015-09-01'
Transform: 'Aliyun::Serverless-2018-04-03'
Resources:
  BlogFC:
    Type: 'Aliyun::Serverless::Service'
    Properties:
      Description: A trivial blog.
      Role: '{ROLE}'
      VpcConfig:
        VpcId: {VpcId}
        VSwitchIds:
          - {VSwitchId}
        SecurityGroupId: {SecurityGroupId}
      NasConfig:
        UserId: 1000
        GroupId: 1000
        MountPoints:
          - ServerAddr: '{ServerAddr}:/'
            MountDir: /mnt/nas
      InternetAccess: true

    typecho:
      Type: 'Aliyun::Serverless::Function'
      Properties:
        Handler: index.handler
        Runtime: custom
        CodeUri: './'
        MemorySize: 192
        Timeout: 10
        InstanceConcurrency: 20
      Events:
        httpTrigger:
          Type: HTTP
          Properties:
            AuthType: ANONYMOUS
            Methods: [ 'HEAD', 'POST', 'PUT', 'GET', 'DELETE']

  {CustomDomain}:
    Type: 'Aliyun::Serverless::CustomDomain'
    Properties:
      Protocol: HTTP,HTTPS
      RouteConfig:
        Routes: 
          '/*':
            ServiceName: BlogFC
            FunctionName: typecho

里面具体各个项目的解释都可以在funcraft的GitHub页面找到,并且这个template文件是可以通过控制台生成的。

我推荐先在网页上创建好VPC、安全组、NAS以及一个拥有listRAMfullVPCfullFCfullNAS权限的RAM用户,并在函数控制面板创建一个自定义函数,在其配置页面将NAS手动挂载上去,然后下载配置文件(导出函数配置),你就得到了一个template.yml文件,直接用对应文件的内容来搭配funcraft配置函数。

实际操作

上传博客程序

首先通过fun nas init来初始化NAS,初始化完成后将博客程序和数据库文件放进.fun/nas对应文件夹下,再通过fun nas sync将文件上传

下载caddy

下载caddy并放到目录里
配置文件:

:{$FC_SERVER_PORT} {
    proxy / localhost:65530 {
        header_upstream Host capriccio.moe
        header_upstream X-Real-IP {>X-Forwarded-For}
        header_upstream X-Forwarded-For {>X-Forwarded-For}
    }
}

:65530 {
    root /mnt/nas/typecho
    gzip

    rewrite  {
        if {path} not_has admin
        to {path} {path}/ /index.php
    }
    fastcgi / 127.0.0.1:65535 php {
        root /mnt/nas/typecho
    }
    header /img Cache-Control "max-age=2678400"
    header /usr/themes/handsome/usr/img Cache-Control "max-age=15552000"
    header /usr/themes/handsome/assets  Cache-Control "max-age=15552000"
}

我通过header_upstream的Host欺骗typecho程序让它以为域名是capriccio.moe,实际上函数的域名并不是这个,capriccio.moe是CDN的域名,如果不做这个trick会有很多毛病(rewrite部分实现伪静态;最后三行header是为了给静态资源加上Cache-Control头。

另外还有一点,typecho似乎默认会把{remote}当作用户的ip,导致所有的ip记录都是"::1",为了解决这个问题,需要在config.inc.php中增加如下内容(参考阿里云开发者社区):

/** 防止CDN造成无法获取客户真实IP地址 */
if(isset($_SERVER['HTTP_X_FORWARDED_FOR']))
{
    $list = explode(',',$_SERVER['HTTP_X_FORWARDED_FOR']);
    $_SERVER['REMOTE_ADDR'] = $list[0];
}

bootstrap

这个抄的阿里云给的wordpress一元建站的,注意到我实际上把php和caddy的配置文件都放到了conf目录里,可以自行调整

#!/bin/bash
set +e

php-fpm7.2 -c /code/conf/php.ini -y /code/conf/fpm.conf
/code/caddy -quiet -conf /code/conf/caddy.conf

while true
do
    php_fpm_server=`ps aux | grep php-fpm | grep -v grep`
    if [ ! "$php_fpm_server" ]; then
        echo "restart php-fpm ..."
        php-fpm7.2 -c /code/conf/php.ini -y /code/conf/fpm.conf
    fi
    caddy_server=`ps aux | grep caddy | grep -v grep`
    if [ ! "$caddy_server" ]; then
        echo "restart caddy ..."
        /code/caddy -quiet -conf /code/conf/caddy.conf
    fi
    sleep 10
done

另外fpm.conf我自己精简了下:

[global]
error_log = /dev/null

[www]
user = www-data
group = www-data
listen.owner = www-data
listen.group = www-data
listen = 127.0.0.1:65535

pm = dynamic
pm.max_children = 5
pm.start_servers = 2
pm.min_spare_servers = 1
pm.max_spare_servers = 3

;php_admin_value[error_log] = /var/log/fpm-php.www.log
;php_admin_flag[log_errors] = on
;php_admin_value[memory_limit] = 32M

部署

首先fun build,该命令需要安装docker,经过一段时间build完成;

完成后fun deploy,你将会看到一些提示,包括打包后的函数包大小,和部署后的更改项。

CDN等

在阿里云CDN控制台中可以直接添加回源目标为函数计算的加速域名,实际测试来看,CDN回源函数计算似乎走的是阿里云内网,不会再单独计费,nice!

由于我的博客域名没有备案(moe顶域也不能备),只能使用国际CDN,大陆访问速度其实不是很快,不过无所谓了(そんなのも関係ないですよね~)

成本

四月份博客支出为0.00元,这个月目前已经花了1分钱,原因是HTTPS请求数较多(

建议改成:零 元 建 站

大致就是以上这些步骤,如果有什么疑惑可以再参照一下阿里云的1元建站

Last modification:July 20, 2020
(๑´ڡ`๑)