又拍云叶靖:OpenResty 在又拍云存储中的应用

2019 年 7 月 6 日,OpenResty 社区联合又拍云,举办 OpenResty × Open Talk 全国巡回沙龙·上海站,又拍云平台开发部负责人叶靖在活动上作了《OpenResty 在又拍云存储中的应用》的分享。OpenResty x Open Talk 全国巡回沙龙是由 OpenResty 社区、又拍云发起,邀请业内资深的 OpenResty 技术专家,分享 OpenResty 实战经验,增进 OpenResty 使用者的交流与学习,推进 OpenResty 开源项目的发展。活动将陆续在深圳、北京、武汉、上海、成都、广州、杭州等城市巡回举办。html

叶靖,又拍云平台开发部负责人,目前主要负责又拍云弹性云处理平台以及内部私有云的设计和开发工做,兼部分文件上传接口相关的工做。对 Python/Lua/Go 等语言有较深刻的研究,在 ngx_lua 和 OpenResty 模块开发方面有丰富经验,专一于高并发、高可用服务架构设计,对 Docker 容器有较多的实践。平时热衷于参与开源社区分享开源经验。git

如下是分享全文:github

你们好,我是又拍云叶靖,今天与你们分享 OpenResty 在又拍云存储系统中的应用,一方面介绍 OpenResty 的应用,另外一方面会介绍又拍云存储系统的原理,又拍云使用 OpenResty 来实现云存储的网关层和 API 接入层。算法

分布式存储,尤为是公有云存储系统都离不开三个要求:sql

  • 高可用,系统不管如何都不可能出现不可服务的状态,即便机器挂了几台,都应该能够写入,并且须要尽量地能够读取;
  • 易扩展,存储的容量是在不断上升的,并且上升的速度很是快,若是系统不能支持快速、方便的扩展,整个系统在运维上会面临很大的压力;
  • 易维护,存储系统有不少组件,这些组件必需要很是容易维护,不能有太多相互的依赖。

存储数据I:拆分

分区

存储数据的拆分,第一步要作分区,这在分布式系统里是很是重要的概念,也是最经常使用的作法。在一个大型数据库中,一般会把整个数据库分红一个个小的子集,最经常使用的作法就是按 key 分区。对于一个云存储系统,key 就是 url 后面的 path,又拍云就是根据 url 后面的 path ,按 A 到 Z 来排列,把数据进行分区。这样分区能够方便地作前缀扫描,由于咱们常常会作目录,列目录无非就是相同的 path 前缀的一些文件,若是 key 是有序排列,这个操做就会很是方便。数据库

第二步操做是须要对 key 进行 hash,来把访问打散,下文会详细介绍。api

上面的这些工做在又拍云都是用 Lua 代码来写的,由 OpenResty 完成数据拆分。
缓存

上图是的 key 的 hash,云存储文件原始的请求是一个 url,可是若是写到存储时也用这个 url 做为它的 key 会形成热点很是严重。又拍云有超过 50 万的付费客户,通过咱们观察,其中有不少客户,尤为是大客户,他们文件的 key 都是带日期的,所以文件的 key 的前缀可能都是某一个日期,而最近上传的文件确定是最热的,这就会致使今天上传的文件所有放在同一台机器上,会使这台机器的带宽被撑满。所以咱们把文件的 url 变成一个 hash,这个 hash 并非 key 的 MD5,也不是某种算法算出来的 hash,其实就是内部生成的一个 UUID 对应这个文件,而后把对应关系记录下来。架构

索引的拆分

索引在存储系统里是文件的元数据信息,元数据信息是指这条记录原始的 key、内部的 key、文件大小、存在于哪些集群内等相似的信息。并发

上图的流程是外部存储访问的上传文件流程。首先是发一个 put 请求,put 请求的 url 就是文件的 key。接着到 OpenResty 层,这层是基于 OpenResty 作的存储网关,存储网关会把 url 生成一个内部的 UUID 并作对应。生成以后会带着 UUID 作上传,接收数据,这里咱们是用 Lua 来作,ngx_lua 里面有一个 req.socket,它会拿到 socket 而后读取上传的数据,数据读到以后存到一个叫 Block 集群内,Block 集群是真正存放文件二进制的地方。整个过程是流式的,边读边写,因此不会带来一些大文件的问题,当文件数据存完以后,再把 UUID、一些元数据信息写到 KeyIndex (元数据集群)。

内容内部拆分

第二步要对 Block 数据进行拆分,前面提到的只是一个简单的过程,其实在接收上传数据并写到 Block 集群的过程当中,并非把全部数据都写到同一个 Block 集群中,而是会作拆分。又拍云支持最大 40T 的文件上传,如今用的磁盘最大也就单个 8T,单文件 40T 是如何支持呢?作法实际上是把 Block 作一个拆分,假如把这个数据拆分红 10M、10M 的块,能够把他上传到不一样的机器和磁盘,只要记录下它的对应关系就能够了。

实际上在 OpenResty 网关里,接收数据的原理也是如此,先收一个 Block 大小,好比 10 M,而后 10M 变成一个 UUID-0 写到 Block 里,再收第二个 10 M,变成 UUID-1,写到 Block 集群,一直到接收完毕,这样一个 G 的文件可能就产生了 100 多个 “UUID-数字”的分块文件,他们分别被存到不一样的机器、磁盘里,这样就能支持超大的文件存储了。

接收数据并写入数据的过程实际上是有策略的。不一样于通常的 OpenResty 用法,好比在作一些鉴权操做、限速操做,只要是在 access 阶段或者 rewrite 阶段去作一些控制,后面就交给 Nginx proxy_pass 作代理,把数据代理出来就能够了;而在这里是彻底没有走 Nginx proxy_pass,直接用 Lua 代码去控制数据的读和写,而后返回,整个的过程都是 Lua 代码去控制。

总的来讲,上面的内容讲了拆分,一共分为三步:

  • 第一次拆分,文件路径(url) 对应多个 Meta 集群,固定分区。在存储里面,Meta 集群是有多个的,Block 集群也有不少个,一个 Meta 集群会对应多个 Block 集群这样的关系。当文件上传上来,它应该存到哪一个集群是有策略的,第一步会对 url 作判断,这个 url 属于哪一个存储分区。咱们常常会在建存储时看到一个选项,建华东数据中心、华南数据中心仍是华北数据中心,此时它已经肯定了,这个存储空间之后的数据永远都是写到哪一个 Meta 集群中,此一次拆分主要作这个事情;
  • 第二次拆分,一个 Meta 集群对应多个 Block 集群,这是 Meta 集群根据内部的一些权重和配置作的调整;
  • 第三次拆分,Meta 和 Block 子系统内部分区,把一个数据分到不一样的磁盘不一样的机器。

存储数据II:路由

第二部份内容,介绍存储里面的路由。

路由模式选择

一般提到路由会想到一种模式就是代理,代理的角色是上图中间的第②种,它中间作了一层代理,全部下面的 MySQL 或 Redis 都只是作单节点的存储,其中前面的代理知道下面全部的节点的存储的分布的路由状况,全部的请求都是通过代理的。

左边第①种模式全部的节点都是对等的,全部节点都知道数据存在哪一个节点上,Redis 在访问的时候就能够随便找一个节点访问,若是数据恰好在这个节点上就直接返回,若是不在这个节点上,此节点会代理到其余的节点上去。

第③种是 Java 生态系统常用的,像 Hbase 就是是使用第③种方式,路由信息存在 client 中,client 直接找到那个节点,省去了好多中间的过程。可是有一个问题是 client 会很是复杂,在存储系统里面,第③种确定是不行的,由于 client 即客户的 rest api ,它只有一个 HTTP,不可能带路由信息。

又拍云选择的是第②种模式,第②种模式中 routing tier 就是 OpenResty 存储网关,它里面有路由信息,知道这个 url 应该去哪一个集群。上图是一个下载文件的 get 请求流程,一个 url 进来后,网关会先去 Meta 集群,即左边的 KeyIndex,拿 url 去找到内部对应的 UUID,而后拿内部的“ UUID-数字”,去 Block 集群里把的分块读出来,而后一块一块流式地吐回去,这就是 get 的过程。

Meta 集群路由都是固定路由,分为几个层次:

  • 不一样的用户或者空间,一个 url 最前面是空间,空间应该对应到哪一个存储集群,这些都是固定的;
  • 不一样存储类型,好比普通的存储、低频的存储它们分别是在哪些集群内都是固定不可改变的;
  • 不一样的索引功能。

列目录

又拍云内部经过网关列目录,简单来讲就是 key 的前缀匹配,咱们建了单独的目录系统来实现目录功能。

上图中左边的 KeyIndex (Meta 集群),里面的数据会实时同步,把一些须要的信息同步到目录索引中,好比列目录只须要文件的 key 的名称、大小、类型、修改时间等,它会把这些信息抽取出来输入到目录系统里面,若是前面的网关收到的是一个列目录的请求,就会直接去目录系统里面,根据前缀匹配把数据列出来。

文件按时间过滤

咱们常常会碰到一个需求,要按照文件的上传时间来列最近上传的文件,或者某天上传的文件,亦或是一年前的上传文件。此时须要单独再建一套按时间排列的索引,不一样于本地的文件系统,本地的文件系统少,要怎么列就怎么列,而云存储文件数量都是千亿以上级别的,若是不事先作好索引,等请求到了再去列,是不可能完成的。

路由

Block 集群的路由和 Meta 集群同样,也是按照存储类型和用户空间划分。此外,不一样的 Block 集群能够有不一样的 Weight ,来控制它不一样的写入量,若是一个新加的 Block 集群须要多写一点数据,就能够把它的 Weight 调高。

又拍云很早以前就开源了一个模块 lua-resty-checkups(https://github.com/upyun/lua-resty-checkups),路由在 OpenResty 里面就是经过这个模块来实现,这个模块在又拍云几乎全部的 ngx_lua 的机器上都有,已经用了好几年了,很是稳定。这个模块主要的工做是管理 upsteam 地址,去作主动的健康检查、被动的健康检查、动态更新 upsteam 地址以及路由的策略等,前面提到的全部的路由功能都是经过这个模块来实现。

又拍云把路由的配置放在 consul 里面,OpenResty 网关会定时去 consul 里面拿最新的配置,而后缓存到本身的进程里面,目前咱们是一分钟拿一次路由配置,缓存到进程里,每一个进程都按照这份配置来工做。关于配置功能,又拍云还开源了一个项目 slardar (https://github.com/upyun/slardar),这里的配置原理和 slardar 原理如出一辙,并且不少模块是直接拿过来的。

存储数据III: 经常使用功能

前面介绍了上传、下载和列目录,咱们常用的还有 Head 操做,Head 操做是检查文件存不存在,它和文件真实的数据没有关系,Head 过来后网关就会拿这个 url 去检查,看是否有这个文件,若是存在就 200 ,不存在就 404 。

DELETE

Delete 操做是不须要 Block 数据的参与的,由于在一个存储系统里面,Delete 并无从磁盘上把数据真正地删除掉,这里的删除只是在元数据库 KeyIndex 里面作一个标记,把这个文件标记为删除。而数据的清理实际上是经过一个异步的 worker 来收集已经被标记删除的文件,而后去 GC 把它们真正地删除掉,而且 GC 会有延迟,并非标记删除就立刻就去 GC,由于有可能会遇到一些误操做的状况,为了不这种状况,咱们一般会把 GC 延迟 7 天甚至 1 个月。整个过程,网关会经过 Lua 中的 kafka 模块,发消息到 kafka 队列,代表这是一次删除操做,kafka 这条消息就会被 GC 的消费者消费,当它拿到这条日志就会定时,时间到了就会去 Block 数据里面把这个文件真正删除。

其余经常使用功能

存储系统里除了刚才说的操做以外,还有不少其余的操做:

  • Move,重命名;
  • Copy,拷贝;
  • Append,追加写;
  • Patch,修改一些文件的源信息;
  • Mkdir,建目录;
  • Random,随机写;

Random 功能目前咱们尚未实现,可是随机读的功能是能够的。除了 Random 功能,其余的都是能够经过 Lua 代码来实现的,这些是用 OpenResty 来写业务逻辑的很好的一个例子。

存储数据IV: 扩容

接下来介绍存储的扩容,这部份内容和 OpenResty 关系不大,可是是存储必定要讲的一个问题。扩容涉及两个方面,一个是 Meta 集群的扩容,另外一个是 Block 集群的扩容。

Meta 集群的扩容

Meta 集群存的是文件的元数据信息,value 其实很是小,可能就只有几百个字节,再大也大不过 1K,它的扩容是相对容易的,好比加一台机器,它的总量也小,balance 速度很是快。

事实上,咱们通常不会作 Mata 集群的扩容,印象中又拍云这么多年只作过一次,由于 Meta 集群的容量能够算出来的,好比要支持一千亿条文件的存储,能够计算出大概须要的 Meta 集群的容量,几百个 T 确定够了,所以你买一批设备放在那,就不用考虑扩容的事情了。总的来讲,Meta 集群的扩容是比较简单的。

Block 集群的扩容

相对来讲比较麻烦的是 Block 集群的扩容。Block 的文件可大可小,它的容量很是大,几十个P,甚至几百个 P。若是你的一个集群有好几个 P,当你加一台机器要从新 balance ,全部的其余的机器要挪出一部分的数据来写到当前你新加的这台机器上,这是一件很是恐怖的事情,可能会须要几天甚至一星期,整个集群都处于一种数据倒来倒去的状态,这是确定会影响业务的。

咱们要尽可能避免这种 balance 的操做,因而想了一种比较取巧的办法,尽可能不作集群内部的 balance,当须要扩容时,就直接新增一个集群。固然有时候也是须要作 balance,若是必定要加就让它慢慢扩,扩几天或者一星期。可是咱们通常的作法是估算出下一个集群须要多少机器、多少容量,直接整个集群上去,在网关层把整个集群配进去,而后调高 Weight 值,让大量的数据都写到新的集群中,这样去作整个云存储的扩容。

其余V

复制

不管是 Meta 集群仍是 Block 集群,都须要有复制的能力,由于咱们都是使用多副本存储,或者 EC 存储。Meta 集群能够选用 Hbase,Postgresql/Mysql,Hbase 有 HDFS 能自带复制功能,而若是是 Postgresql/Mysql,须要配置它的主从或者给它作一些同步、复制的功能。

此外,Meta 数据的备份也很重要,由于 Meta 集群关系到全部的数据是否可以访问,一旦出现问题就会很是严重,因此这里就须要在网关层把 Meta 数据写到 kafka,另一种办法是直接在数据库弄个插件,再导到 kafka。

Block 集群的复制比较复杂,一般是集群内部要完成的事情,和网关层没有太大的关系。

事务

事务也是存储很是重要的概念,在云存储系统中,没有办法作到像单机数据库那样的事务,它只能作到单个对象级别的事务,保证这个对象是处在事务里面的。整个操做是须要一个 Meta 集群支持一个 CAS(compare-and-set)操做。一个对象不能被两个线程同时写入,这样会形成其中一个线程失败,会之后面写入的 Meta 信息为准。

前面提到一个 Key 只能一次被写入,这里会涉及到限速,咱们使用的是 openresty/lua-resty-limit-traffic,又拍云在此基础上增长了 token bucket 的方法,token bucket 这个模块目前也是开源放在咱们的 github 上,咱们内部都是用这个模块,测试下来这个模块是最平滑的,能很好应对突发的请求。

分布式存储以外

前面介绍的都是存储的网关层、以及存储下面的功能,其实作一个云存储系统,不仅仅是作一个网关或存储,后面还有许多配套的东西,好比 API,API 又拍云也是经过 Lua 来写的,这里也有不少的业务逻辑,好比表单 API 涉及到表单的解析、参数的解析、上传到存储网关等。此外,还有认证的算法、断点续传也都是经过 Lua 来写的。断点续传,是指一个大文件如十几个 G 的文件,能够把它切成 1M、1M 的文件块分别传到存储,存储会先把这些文件写到 Block 集群,当接收到最后一个 finish 消息,存储就会把这些临时的数据拼成一整个文件。

又拍云存储系统

上图是又拍云存储系统模块关系图,OpenResty 在里面主要是左上角这块,UpyunApi 是又拍云的 API 层,像认证、鉴权、上传的表单 API 等都是它作的事情;Avalon 是 OpenResty 的云存储网关,内部与存储相关的流量都会通过这里,包括 CDN 的 get 流量也会通过这里;左边的是 Meta 集群,它有不少组件,包括 Hbase、Postgresql、Redis 以及备份的工做;右边的是一些消费者,由于存储系统须要不少的消费者来完成一些特定的工做,好比自动过时、TTL、GC、坏盘的修复等;最下面的部分是 Block 集群,是真正存数据的地方。

又拍云 OpenResty 相关的开源项目

下面是前面提到的一些又拍云开源出来的开源项目,这些在 upyun 的仓库里面均可以找到,又拍云内部也是大量使用这些模块,主要包括:

[1] upyun/slardar : https://github.com/upyun/slardar

[2] upyun/lua-resty-checkups : https://github.com/upyun/lua-resty-checkups

[3] upyun/lua-resty-limit-rate :https://github.com/upyun/lua-resty-limit-rate

演讲视频及PPT下载:

OpenResty 在又拍云存储中的应用 - 又拍云

相关文章
相关标签/搜索