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

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

现有资源:
1.一个成熟的基因对比算法(Python实现,运行一次的时间花费为 2 秒)
2.2020 个基因样本文件(每一个文件的大小为 2M,能够直接做为算法的输入)
3.一台 8 核心云主机算法

<strong>基因检测服务</strong>api

咱们使用上面的资源来对比两我的的基因样本并 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

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

<strong>咱们接到了以下业务需求</strong>
假设有 2000 人寻找本身的孩子,20 人寻找本身的父亲。less

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

2000 * 20 = 40000(次)异步

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

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

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

22.2 / 8 ≈ 2.76(小时)

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

<strong>1.介绍一种 Serverless 产品:UGC

与 AWS 的 lambda 不一样,UGC 容许你将计算密集型算法封装为 Docker Image (后文统称为「算法镜像」),只需将算法镜像 push 到指定的算法仓库中,UGC 会将算法镜像预先 pull 到一部分计算节点上,当你使用如下两种形式:
•算法镜像的名字和一些验证信息经过 querystring 的形式 (例如:http://api.ugc.service.ucloud.cn?ImageName=relation&amp; AccessToken=!Q@W#E)
•算法镜像所需的数据经过 HTTP body 的形式

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

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

<strong>2.针对此 Serverless 架构的改造</strong>

两部分:
1.改造算法中元数据从「文件输入」改成「标准输入」,输出改成「标准输出」
2.开发客户端构造 HTTP 请求,并提升并发

<strong>1. 改造算法输入输出</strong>

① 改造输入为 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)


到此就改造完了,很快吧

<strong>2. 客户端与并发</strong>

刚才咱们改造了算法镜像的逻辑(任务的执行),如今咱们来看一下任务的提交:
构造 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 秒,从以前的 7 天提升到 400 秒,成果斐然,图表更直观:

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

最重要的是改造后的架构还继承了 Serverless 架构的优势:免运维、高可用、按需付费、发布简单。

<strong>免运维</strong> -- 由于你没有服务器了...

<strong>高可用</strong> -- Serverless 服务通常依托云计算的强大基础设施,任何模块都不会只有单点,都尽量作到跨可用区,或者跨交换机容灾,并且本次使用的服务有一个有趣的机制:同一个任务,你提交一次,会被多个节点执行,若是一个计算节点挂了,其余节点还能够正常返回,哪一个先执行完,先返回哪一个。

<strong>按需付费</strong> -- 文中说每一个算法执行一次花费单核心 CPU 时间 2 秒,咱们直接算一下花费:

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

<strong>发布简单</strong> -- 由于使用 Docker 做为载体,因此它是语言无关的,并且发布也很快,代码写好直接上传镜像就行了,至于灰度,客户端 imageName 指定不一样版本便可区分不一样代码了

不一样的 Serverless 产品可能有不一样的改造方法,做为工程师,我比较喜欢这种方式,改形成本低,灵活性高,你以为呢?若是对 Serverless 架构或者 UGC 感兴趣的话能够添加微信 u_nknow 加入咱们的技术交流群!

相关文章
相关标签/搜索