《架构师》2018年2月
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

特别专栏 | Column

一次Serverless架构改造实践:基因样本比对

作者 高远

Serverless是一种新兴的无服务器架构,使用它,开发者只需专注于代码,无需关心运维、资源交付或者部署。本文将从代码的角度,通过改造一个Python应用来帮助读者从侧面理解Serverless,让应用继承Serverless架构的优点。

现有资源:

· 一个成熟的基因对比算法(Python实现,运行一次的时间花费为2秒)

· 2020个基因样本文件(每个文件的大小为2M,可以直接作为算法的输入)

· 一台8核心云主机

基因检测服务

我们使用上面的资源来对比两个人的基因样本并print对比结果(如:有直系血缘关系的概率)。

我们构造目录结构如下:

                ├── relation.py
        └── samples
            ├── one.sample
            └── two.sample
       relations.py代码如下:
        import sys
 
        def relationship_algorithm(human_sample_one, human_sample_two):
            # it's a secret
            return result
        if __name__ == ‘__main__':
            length = len(sys.argv)
            # sys.argv is a list, the first element always be the script's
        name
            if length ! = 3:
                sys.stderr.write(‘Need two samples')
            else:
                # read the first sample
                with open(sys.argv[1], ‘r') as sample_one:
                    sample_one_list = sample_one.readlines()
                # read the second sample
                with open(sys.argv[2], ‘r') as sample_two:
                    sample_two_list = sample_two.readlines()
                # run the algorithm
                  print relationship_algirithm(sample_one_list, sample_two_
        list)
使用方法如下:
        python relation.py ./samples/one.sample ./samples/two.sample
        0.054

流程比较简单,从本地磁盘读取两个代表基因序列的文件,经过算法计算,最后返回结果。

我们接到了如下业务需求

假设有2000人寻找自己的孩子,20人寻找自己的父亲:

首先,收集唾液样本经过专业仪器分析后,然后生成样本文件并上传到我们的主机上,一共2020个样本文件,最后我们需要运行上面的算法:

        2000 * 20 = 40000(次)

才可完成需求,我们计算一下总花费的时间:

        40000(次)* 2(秒)= 80000(秒)
        80000(秒)/ 60.0 / 60.0 ≈ 22.2(小时)

串行需要花费22小时才能算完,太慢了,不过我们的机器是8核心的,开8个进程一起算:

        22.2 / 8 ≈ 2.76(小时)

也要快3个小时,还是太慢,假设8核算力已经到极限了,接下来如何优化呢?

一种Serverless产品:UGC

与AWS的lambda不同,UGC允许你将计算密集型算法封装为Docker Image(后文统称为「算法镜像」),只需将算法镜像push到指定的算法仓库中,UGC会将算法镜像预先pull到一部分计算节点上,当你使用以下两种形式:

算法镜像的名字和一些验证信息通过querystring的形式(例如:http://api.ugc.service.ucloud.cn? ImageName=relation&Token=! Q@W#E)

· 算法镜像所需的数据通过HTTP body的形式

特别构造的HTTP请求发送到UGC的API服务时,「任务调度器」会帮你挑选已经pull成功算法镜像的节点,并将请求调度过去,然后启动此算法镜像「容器」将此请求的HTTP body以标准输入stdin的形式传到容器中,经过算法计算,再把算法的标准输出stdout和标准错误stderr打成一个tar包,以HTTP body的形式返回给你,你只需要把返回的body当做tar包来解压即可得到本次算法运行的结果。

UGC产品架构图

讲了这么多,这个产品使你可以把密集的计算放到了数万的计算节点上,而不是我们小小的8核心机器,有数万核心可供使用,那么如何使用如此海量的计算资源呢,程序需要小小的改造一下。

针对此Serverless架构的改造

两部分:

改造算法中源数据从「文件输入」改为「标准输入」,输出改为「标准输出」

开发客户端构造HTTP请求,并提高并发

1. 改造算法输入输出

① 改造输入为stdin

        cat ./samples/one.sample ./samples/two.sample | python relation.py

这样把内容通过管道交给relation.py的stdin,然后在relation.py中通过以下方式拿到:

        import sys
        mystdin = sys.stdin.read()
        # 这里的mystdin包含 ./samples/one.sample ./samples/two.sample的全
        部内容,无分隔,实际使用可以自己设定分隔符来拆分

② 将算法的输出数据写入stdout

        # 把标准输入拆分为两个sample
        sample_one, sample_two = separate(mystdin)
         # 改造算法的输出为stdout
        def relationship_algorithm(sample_one, sample_two)
            # 改造前
            return result
            # 改造后
            sys.stdout.write(result)

到此就改造完了,很快吧。

2. 客户端与并发

刚才我们改造了算法镜像的逻辑(任务的执行),现在我们来看一下任务的提交:

构造HTTP请求并读取返回结果。

        imageName  =  ‘cn-bj2.ugchub.service.ucloud.cn/testbucket/
        relationship:0.1’
        token = tokenManager.getToken() # SDK有现成的
 
        # summitTask构造HTTP请求并将镜像的stdout打成tar包返回
        response = submitTask(imageName, token, data)

它也支持异步请求。

之前提到,此Serverless产品会将算法的标准输出打成tar包放到HTTP body中返回给客户端,所以我们准备此解包函数:

        import tarfile
        import io
        def untar(data):
            tar = tarfile.open(fileobj=io.BytesIO(data))
            for member in tar.getmembers():
                f = tar.extractfile(member)
                with open(‘result.txt', 'a') as resultf:
                    strs = f.read()
                    resultf.write(strs)

解开tar包,并将结果写入result.txt文件。

假设我们2200个样本文件的绝对路径列表可以通过get_sample_list方法拿到。

        sample_2000_list, sample_20_list = get_sample_list()

计算2000个样本与20个样本的笛卡尔积,我们可以直接使用itertools.product。

        import itertools
        all = list(itertools.product(sample_2000_list, sample_20_list))
         assert len(all) == 40000

结合上面的代码段,我们封装一个方法:

        def worker(two_file_tuple):
            sample_one_dir, sample_two_dir = two_file_tuple
            with open(sample_one_dir) as onef:
                one_data = onef.read()
            with open(samle_two_dir) as twof:
                two_data = twof.read()
            data = one_data + two_data
            response = summitTask(imageName, token, data)
            untar(response)

因为构造HTTP请求提交是I/O密集型而非计算密集型,所以我们使用协程池处理是非常高效的:

        import gevent.pool
        import gevent.monkey
        gevent.monkey.patch_all() # 猴子补丁
        pool = gevent.pool.Pool(200)
        pool.map(worker, all)

只是提交任务200,并发很轻松。

全部改造完成,我们来简单分析一下:

之前是8个进程跑计算密集型算法,现在我们把计算密集型算法放到了Serverless产品中,因为客户端是I/O密集型的,单机使用协程可以开很高的并发,我们不贪心,按200并发来算:

并发对比图

进阶阅读:上面这种情况下带宽反而有可能成为瓶颈,我们可以使用gzip来压缩HTTP body,这是一个计算比较密集的操作,为了8核心算力的高效利用,可以将样本数据分为8份,启动8个进程,进程中再使用协程去提交任务就好了。

        40000 * 2 = 80000(秒)
        80000 / 200 = 400(秒)

也就是说进行一组检测只需要400秒,从之前的22小时提高到400秒,成果斐然,图表更直观。

而且算力瓶颈还远未达到,任务提交的并发数还可以提升,再给我们一台机器提交任务,便可以缩短到200秒,4台100秒,8台50秒...

最重要的是改造后的架构还继承了Serverless架构的优点:

· 免运维

· 高可用

· 按需付费

· 发布简单

花费时间对比图

1. 免运维--因为你没有服务器了……

2. 高可用--Serverless服务一般依托云计算的强大基础设施,任何模块都不会只有单点,都尽可能做到跨可用区,或者跨交换机容灾,而且本次使用的服务有一个有趣的机制:同一个任务,你提交一次,会被多个节点执行,如果一个计算节点挂了,其他节点还可以正常返回,哪个先执行完,先返回哪个。

3. 按需付费--文中说每个算法执行一次花费单核心CPU时间2秒,我们直接算一下花费。

        2000 * 20 * 2 = 80000(秒)
        80000 / 60 / 60 = 22.22(小时)
        22.22(小时)* 0.09(元)* 1(核心)~= 2(元)
        每核时0.09元(即单核心CPU时间1小时,计费9分钱)

发布简单--因为使用Docker作为载体,所以它是语言无关的,而且发布也很快,代码写好直接上传镜像就好了,至于灰度,客户端imageName指定不同版本即可区分不同代码了。

不同的Serverless产品可能有不同的改造方法,作为工程师,本人比较喜欢这种方式,改造成本低,灵活性高,你觉得呢?欢迎展开讨论。

作者简介

高远,拥有多年DevOps经验,UCloud实验室自动化运维运营平台负责人,干净、易懂代码的践行者,希望为计算机科学的发展尽一份力。(知乎ID:临书)