是时候该对你的数据库做一次全面体检啦!

December 26th, 2016    阅读(77) No comments

数据库作为一个大型的并发存储系统,其内部设计极其复杂,开发者在使用数据库时,面临着可用性、可靠性、性能、安全、扩展性等多重挑战,这就使得数据库的使用具有较高的技术门槛。一般大型团队都会雇佣专职的DBA来维护数据库的日常运行,指导开发者建立正确的使用姿势,但是对于一般的中小型团队,受限于人力成本,这种模式难以实现。随着DevOps理念的流行,开发者开始更多的参与和承担数据库的运维工作,其中就包括对数据库的定期巡检。

对数据库定期进行健康检查是数据库日常维护的重要环节,通过检查数据库的各项运行指标,评估系统的运行风险,提前将风险消灭在摇篮中,能够有效提高数据库服务的质量,今天我们就来聊聊开发者如何对数据库进行健康检查。

数据库的健康检查涉及索引设计、容量规划、服务安全、参数配置、用户访问、集群复制6个方面。

 

索引设计

合理的索引设计能够有效加速数据库的访问,提高查询的执行效率,减少用户查询对服务端的资源消耗。但是不合理的、低效的、冗余的甚至无效的索引不仅无法起到加速查询的效果,反而会影响数据库的插入、更新性能,甚至是数据库的高可用方案能否生效。

  • 主键索引缺失: 由于MySQL默认存储引擎InnoDB(MySQL 5.6版本 以上)使用的是聚簇索引表设计,这就要求所有的表必须包含一个主键,所有的数据记录按照主键次序构建B+树。如果用户在创建表时显式指定主键,则数据库会使用用户指定的主键构建B+树,但是如果用户没有显式指定主键,同时也没有创建任何唯一键索引,InnoDB为了确保每张表至少包含一个主键,则默认会为用户生成一个“隐含主键”,该主键对用户不可见,甚至对于MySQL Server层的binlog也不可见。binlog是连接MySQL主从复制节点的纽带,所有主节点的更新都是通过binlog传递给从节点的,一旦binlog中没有更新记录的主键ID,这就会导致基于Row格式的binlog在从节点执行时,无法唯一确定一条记录,只能通过全表扫描来进行匹配,大幅降低了从机的执行效率,造成复制延迟。如果是高可用故障切换的从节点,会导致切换的时间大幅增加,甚至会导致高可用机制失效。如果是实现读写分离的只读从节点,则会导致应用读到的数据可能是很久以前的旧数据。所以我们建议使用InnoDB存储引擎的MySQL用户在创建表时,必须显式指定主键。

image

  • 主键索引与业务相关:如果用户在创建表时指定的主键与业务相关,可能会被频繁的更新,这样会引起MySQL数据库的InnoDB存储引擎进行频繁的节点合并和分裂,造成大量额外的系统IO开销,影响数据库的插入和更新性能。我们推荐开发者在创建表时指定与业务无关的自增字段作为主键,这样不仅会提高按时间序插入的性能(顺序写入硬盘),同时也可以提高按插入时间范围检索的查询效率。
  • 冗余索引:如果一个索引涉及的字段属性包含另外一个索引涉及的字段属性,同时两个索引字段顺序一致,且两个索引的首字段属性相同,则可以认为涉及字段少的索引为冗余索引。在MySQL 5.7推出sys库之前,我们可以通过percona的工具pt-duplicate-key-checker来完成对冗余索引的检查,在MySQL 5.7中,我们可以通过sys库schema_redundant_indexes表来完成。

image

  • 低效索引:索引的作用在于通过索引,查询能够扫描更少的记录。数据库中的记录在索引字段区分度越高,扫描的记录数就越少,执行的效率就越高。如果数据库表中的记录在索引字段区分度不大,索引对记录的筛选结果就不明显,索引就无法起到加速查询的作用。通过数据库记录在索引字段的区分度,我们可以衡量索引的执行效率。MySQL系统库mysql库下,innodb_index_stats表的stat_value字段,记录了某张表在某个索引的不同取值的记录个数,innodb_table_stats表的n_rows字段记录了某张表的总记录数,二者相除,即可得到数据库记录在某个索引的区分度,越接近1,表示区分度越高,低于0.1,则说明区分度较差,开发者应该重新评估SQL语句涉及的字段,选择区分度高的多个字段创建索引,通过运行下面的SQL语句,就可以计算每张表的索引区分度。

chip_201612261359307

  • 无效索引:如果一个索引始终无法被查询使用,它的存在只能增加数据库的维护开销,开发者应该及时删除这些索引。通过MySQL 5.7 sys库schema_unused_indexes视图,可以查看当前实例哪些索引从没有被使用。

 

容量规划

数据库的运行依赖计算、存储、网络等多种资源,通过对各种资源的使用情况分析,对资源进行合理的规划配置,是数据库稳定运行的必要条件。

  • CPU: 通常使用CPU利用率衡量CPU的繁忙程度,通过top命令,开发者可以查看CPU利用率实时变化。CPU 利用率持续超过80%,预示计算资源已经接近饱和,如果开发者已经做过SQL优化,则需要使用更高配置的CPU。通过查看7天内CPU利用率超过80%的时间占整体时间的百分比,以及单次持续时间超过一定阈值,则可视为CPU扩容的触发条件。
  • IO:大部分数据库应用都是的IO Bound类型,IO 处理能力直接决定了数据库的性能。IO 利用率统计了一秒内IO请求队列非空的时间比例,IO利用率越高就表示硬盘越繁忙。但是IO 利用率100%并不表示系统已经无法处理更多的IO请求。IOPS和每秒IO字节数可以从存储设备的层次更准确的描述IO负载。每一个存储设备都有IOPS和每秒IO字节数的上限,任意一个达到上限,就会成为IO处理能力的瓶颈,在传统机械硬盘中,随机IO主要受到IOPS的限制,顺序IO主要受带宽限制。除此之外,我们还可以从应用的角度,使用一次IO请求的响应时间来描述IO负载,一次IO请求的响应时间包括其在队列中的等待时间和实际IO处理时间之和。通过iostats开发者可以很方便的收集这些数据。如果这些指标在一段时间内持续接近设定上限,则可以认为IO 过载,通过扩大内存,让更多的读写请求命中缓存可以缓解硬盘IO。另外,使用更高配置的存储设备,例如固态硬盘,也可以大幅提高系统的IO处理能力。
  • 存储空间:存储空间不足会导致严重的系统故障,数据库可能会宕机,更为严重的是数据库进程存活,但是无法响应服务,从而造成基于进程的宕机监控失效。根据7天内数据库中存储数据的变化,我们可以按照一定的拟合算法,估算出未来3天内数据的增长情况,来判断实例是否存在存储空间不足的风险。
  • 内存:使用InnoDB存储引擎的MySQL数据库在实例启动时,就会预分配一块固定大小的内存空间,所有读写请求都会在该空间中完成,如果内存中缓存了用户读写的数据,则直接读取内存,如果内存中没有用户读写的数据,则需要将数据先从硬盘中load进内存中,由于内存的读写速度远远快于硬盘,这就使得读写请求是否命中内存决定了读写请求的处理速度。内存空间越大,缓存数据越多,命中的几率也就越大。所以我们可以使用缓存命中率来衡量内存空间大小是否满足应用的需求。在MySQL中,show engine innodb status 命令的Buffer pool hit rate可以度量近一段时间范围内Buffer pool的命中情况。

image

  • 网络:网络带宽在数据库返回记录较多的情况下,也可能会成为系统的瓶颈。一般我们使用每秒网络流入和流出字节数来衡量网络流量是否达到带宽限制。在云环境下,每台虚拟机或者容器都是有一定的网络带宽配额,私有网络的配额相对比较大,公网配额与用户付费相关;使用iftop 可以查看当前系统的网络流量;

 

服务安全

  • 弱密码:MySQL的登陆认证使用的是IP和账户密码的方式,很多开发者为了方便记忆,习惯将数据库密码设置为弱密码,这实际是非常危险的。数据库中的数据很多涉及敏感业务,弱密码非常容易被破解,对数据库中的数据是一个严重安全隐患。MySQL系统库mysql库下的user表的password字段保存了所有用户的密码,MySQL使用的是两次sha-1的不可逆加密算法,所以我们无法通过password字段获取用户的密码内容,但是我们可以通过将常见弱密码制成彩虹表,模拟MySQL的加密算法,匹配password字段,即可发现数据库中的弱密码账号。
  • 网络安全:在一般的业务架构中,数据库都不会直接服务于终端用户,而是服务于运行业务逻辑的应用程序。所以数据库和业务程序之间出于安全的考虑,会选择使用私有网络。即便如此,为了避免数据库连错,也需要在设置数据库账号时,增加IP来源限制。在一些特定的场景下,如果数据访问必须借助公网来实现,就会将数据库暴漏在公网上。使用公网数据库实例,必须要配置防火墙,否则存在被攻击的隐患。通过iptables我们可以控制访问数据库的来源IP。
  • 权限检查:MySQL提供了多种权限配置,为了方便管理以及避免误操作,一般会将管理权限和访问权限配置成两个不同的账号,禁止使用管理权限作为业务程序访问数据库的账号。通过系统库mysql库的user表可以确认各个账号拥有的权限,尽量避免业务账号拥有super权限;

 

参数配置

  • 内存相关参数:MySQL数据库的内存使用包括两个部分:共享内存与连接独占内存。每一个用户新建连接,数据库都要分配一块固定大小的内存空间保存用户的临时数据,这些空间为单个连接独占。在MySQL实例启动时,系统同时也会预先分配一些实例级别的共享内存空间,例如Innodb_buffer_pool,Innodb_log_buffer_pool等,供所有连接共享。独占内存空间乘以最大连接数加上共享内存空间,我们可以计算出MySQL最大可使用的内存空间,如果超过实际物理内存大小,就存在MySQL进程被Linux操作系统强行oom kill风险,导致实例宕机。MySQL的这些内存空间都可以通过配置参数指定大小,如果超过实际内存空间,应该调整相应参数配置,最常见的是调整Innodb_buffer_pool和最大连接数。

chip_201612261344334

  • 频繁卡顿,如果设置过大,会导致数据库实例重启或者故障恢复花费大量的时间。一般,对于使用固态硬盘等高配置的存储设备的数据库,可以将重做日志设置大一些,对于使用机械硬盘的数据库,应该设置小一些,一般在512M到4G之间。innodb_flush_log_at_trx_commit定义了重做日志的刷新节奏,如果该参数非1,会导致数据库宕机重启后丢失部分更新数据,对于数据可靠性要求较高的应用造成严重影响。
  • 二进制日志相关参数:binlog 主要用于MySQL集群复制以及故障恢复担任协调者的作用。binlog_format定义了binlog的格式,主要包括ROW、STATEMENT、MIXED三种格式,ROW格式是最安全的一种日志格式,会保证主从数据的严格一致,建议开发者选用ROW格式。但是ROW格式的binlog会占用更多的存储空间,通过expire_logs_days可以控制保存binlog的天数,如果binlog占用的存储空间比例超过50%,则应考虑适当减少binlog的保存天数。sync_binlog 参数定义了binlog刷新硬盘的节奏,如果非1,会导致宕机重启后最近的更新数据丢失。
  • 连接数相关参数:MySQL有最大连接数限制max_connections,如果应用连接超过max_connetions限制,则会得到out of max connections异常,无法建立连接。show processlist可以查看当前的连接数,如果接近最大限制,则存在无法新建连接的风险。通过在应用端使用连接池可以控制数据库的连接数。

 

用户访问

  • 慢连接:慢查询数量是最直观的反映数据库处理能力是否满足业务需求的指标。通过设置slow_query_log可以开启慢查询日志,MySQL数据库会将执行时间超过long_query_time的查询记入慢查询日志,如果某个时间段内,慢查询数量急剧增加,则开发者就必须要关注数据库的性能问题,首先就需要进行SQL优化,其次考虑资源是否需要扩容,最后可能需要数据库水平扩展方案,包括创建只读从节点;
  • 死锁数量:两个事务涉及的数据库记录有重叠,如果SQL语句的加锁顺序不一致,就会导致事务之间的死锁。虽然MySQL数据库会自动的检测死锁并强制回滚系统认为代价较小的事务,但是死锁的检测与事务回滚都有较大的代价,会严重拖慢数据库的性能,所以当系统中出现大量死锁时,开发者必须引起重视,要分析发生死锁的事务的SQL语句的加锁规则,调整SQL语句。通过show engin innodb status可以查看死锁的相关信息以及系统的处理过程。

image

 

 集群复制

  • 数据安全:复制是MySQL多个节点之间实现数据同步的重要机制,主要用于搭建高可用实例主从节点以及提供多个只读从节点提高读扩展能力。节点之间的数据是否最终一致对于高可用方案是否生效,只读实例读取的数据是否正确有着严重影响。从机执行show slave status可以获取从机的复制状态,Slave_IO_Running和Slave_SQL_Running分别表示IO和SQL线程是否正常运行,如果不正常,则应及时处理。参数relay_log_recovery和relay_log_info_repository影响从节点宕机重启后,与主机的复制位置是否正确,如果位置错误,则可能导致数据错误。
  • 复制性能:复制延迟经常用来评估复制性能是否满足业务需求。Show slave status的Seconds behind master字段标识了从机落后主机的延迟时间。如果延迟较长,则会影响高可用实例主从切换的时间以及只读从节点是否能够及时读到最新数据。通过使用并行复制技术可以提高从节点的复制性能。MySQL 5.6提供了基于Database级别的并行复制,通过slave_parallel_workers 设置并行线程数;MySQL 5.7提供了基于LOGICAL_CLOCK的并行复制, 主机上同一个Group提交的binlog中包含事务在从机并行执行,相比database,具备更高的并发性,除了设置slave_parallel_workers,还需要将slave-parallel-type设置为LOGICAL_CLOCK。slave_preserve_commit_order=1可以确保从机并行执行的事务按序提交。同时从机的log_bin和log_slave_updates参数必须同时开启。

 

网易蜂巢智能数据库健康诊断系统

使用网易蜂巢的开发者,可以使用平台提供的智能健康诊断系统对数据库服务中的关系数据库实例进行自动的健康检查。检查内容覆盖6个大类,22个子项,检查结束后根据检查结果,会自动生成健康指数,开发者根据健康指数,可以快速判断系统存在的风险严重程度,同时平台提供了该分数在所有实例的健康检查中的排名。

image

有风险的项目平台会使用橙色标识,开发者点击风险项目,会看到系统对该风险的详细描述以及相应的修复建议。针对部分检查内容,系统提供了一键自动修复功能。

image

Categories: Uncategorized Tags:

怎样才算精通Python?

December 21st, 2016    阅读(185) No comments
在这篇文章中,我会1)先给出我对精通Python的理解;2)然后给出一些Python中有难度的知识点。如果大家在看完这篇文章之前,已经充分理解了我列出的各个知识点,那么,我相信你已经算是精通Python了。如果不能,我希望这篇回答能让你意识到自己Python知识还存在哪些不足,在之后的学习中,从哪些方面去改进。

一、精通是个伪命题

怎样才算精通Python,这是一个非常有趣的问题。

很少有人会说自己精通Python,因为,这年头敢说精通的人都会被人摁在地上摩擦。其次,我们真的不应该纠结于编程语言,而应该专注于领域知识。比如,你可以说你精通数据库,精通分布式,精通机器学习,那都算你厉害。但是,你说你精通Python,这一点都不酷,在业界的认可度也不高。

再者,Python使用范围如此广泛,可以说是世界上使用领域最广的编程语言(没有之一)。一个人精力有限,不可能精通所有的领域。就拿Python官网的Python应用领域来说,Python有以下几个方面的应用:

  • Web Programming: Django, Pyramid, Bottle, Tornado, Flask, web2py
  • GUI Development: wxPython, tkInter, PyGtk, PyGObject, PyQt
  • Scientific and Numeric: SciPy, Pandas, IPython
  • Software Development: Buildbot, Trac, Roundup
  • System Administration: Ansible, Salt, OpenStack

如果有人真的精通上面所有领域,那么,请收下我的膝盖,并且,请收我为徒。

既然精通Python是不可能也是没有意义的事情,那么,为什么各个招聘要求里面,都要求精通Python呢?我觉得这都是被逼的。为什么这么说呢,且听我慢慢说来。

 

1.1 为什么招聘要求精通Python

绝大部分人对Python的认识都有偏差,认为Python比较简单。相对于C、C++和Java来说,Python确实比较容易学习一些。所以,才会有这么多只是简单地了解了一点语法,就声称自己会Python的工程师。

打个比方,如果一个工程师,要去面试一个C++的岗位,他至少会找一本C++的书认真学习,然后再去应聘。Python则不然,很多同学只花了一点点时间,了解了一下Python的语法,就说自己熟悉Python。这也导致Python的面试官相对于其他方向的面试官,更加容易遇到不合格的求职者,浪费了大家的时间。Python面试官为了不给自己找麻烦,只能提高要求,要求求职者精通Python。

 

1.2 怎样才算精通Python

既然精通Python本身是一件不可能的事情,而面试官又要求精通Python,作为求职者,应该达到怎样的水平,才敢去应聘呢?我的观点是,要求精通Python的岗位都是全职的Python开发,Python是他们的主要使用语言,要想和他们成为同事,你至少需要:

1. 能够写出Pythonic的代码
2. 对Python的一些高级特性比较熟悉
3. 对Python的优缺点比较了解

 

二、敢来挑战吗

前面的几点要求说出来可能比较抽象,不太好理解。我们来看几个例子,如果能够充分理解这里的每一个例子,那么,你完全能够顺利通过”精通Python”的岗位面试。

2.1 上下文管理器

大家在编程的时候,经常会遇到这样的场景:先执行一些准备操作,然后执行自己的业务逻辑,等业务逻辑完成以后,再执行一些清理操作。

比如,打开文件,处理文件内容,最后关闭文件。又如,当多线程程序需要访问临界资源的时候,线程首先需要获取互斥锁,当执行完成并准备退出临界区的时候,需要释放互斥锁。对于这些情况,Python中提供了上下文管理器(Context Manager)的概念,可以通过上下文管理器来控制代码块执行前的准备动作以及执行后的收尾动作。

我们以处理文件为例来看一下在其他语言中,是如何处理这种情况的。 Java风格/C++风格的Python代码:

    myfile= open(r'C:\misc\data.txt')
    try:
        for line in myfile:
            ...use line here...
    finally:
        myfile.close()

Pythonic的代码:

    with open(r'C:\misc\data.txt') as myfile:
        for line in myfile:
            ...use line here...

我们这个问题讨论的是精通Python,显然,仅仅是知道上下文管理器是不够的,你还需要知道:

 

2.1.1 上下文管理器的其他使用场景(如数据库cursor,锁)

  • 上下文管理器管理锁
        class FetchUrls(threading.Thread):
            ...
            def run(self):
                ...
                with self.lock:   #使用"with"语句管理锁的获取和释放
                  print 'lock acquired by %s' % self.name
                  print 'lock released by %s' % self.name
  • 上下文管理器管理数据库cursor
        import pymysql

        def get_conn(**kwargs):
            return pymysql.connect(host=kwargs.get('host', 'localhost'),
                    port=kwargs.get('port', 3306),
                    user=kwargs.get('user'),
                    passwd=kwargs.get('passwd'))

        def main():
            conn = get_conn(user='laimingxing', passwd='laimingxing')
            with conn as cur:
                cur.execute('show databases')
                print cur.fetchall()

        if __name__ == '__main__':
            main()
  • 上下文管理器控制运算精度
        with decimal.localcontext() as ctx:
            ctx.prec = 22
            print(decimal.getcontext().prec)

 

2.1.2 上下文管理器可以同时管理多个资源

假设你需要读取一个文件的内容,经过处理以后,写入到另外一个文件中。你能写出Pythonic的代码,所以你使用了上下文管理器,满意地写出了下面这样的代码:

        with open('data.txt') as source:
            with open('target.txt', 'w') as target:
                target.write(source.read())

你已经做得很好了,但是,你时刻要记住,你是精通Python的人啊!精通Python的人应该知道,上面这段代码还可以这么写:

        with open('data.txt') as source, open('target.txt', 'w') as target:
            target.write(source.read())

 

2.1.3 在自己的代码中,实现上下文管理协议

你知道上下文管理器的语法简洁优美,写出来的代码不但短小,而且可读性强。所以,作为精通Python的人,你应该能够轻易地实现上下文管理协议。在Python中,我们就是要自己实现下面两个协议:

  • __enter__(self)
  • __exit__(self, exception_type, exception_value, traceback)

当然,更优美的方法是使用contextmanager装饰器。

2.2 装饰器

由于我们这里讨论的是精通Python,所以,我假设大家已经知道装饰器是什么,并且能够写简单的装饰器。那么,你是否知道,写装饰器也有一些注意事项呢。

我们来看一个例子:

    def is_admin(f):
        def wrapper(*args, **kwargs):
            if kwargs.get("username") != 'admin':
                raise Exception("This user is not allowed to get food")
            return f(*args, **kwargs)
        return wrapper


    @is_admin
    def barfoo(username='someone'):
        """Do crazy stuff"""
        pass

    print barfoo.func_doc
    print barfoo.__name__

    None
    wrapper

我们用装饰器装饰完函数以后,无法正确地获取到原函数的函数名称和帮助信息,为了获取这些信息,我们需要使用@functool.wraps。 如下所示:

    import functools
    
    def is_admin(f):
        @functools.wraps(f)
        def wrapper(*args, **kwargs):
            if kwargs.get("username") != 'admin':
                raise Exception("This user is not allowed to get food")
            return f(*arg, **kwargs)
        return wrapper

再比如,我们要获取被装饰的函数的参数,以进行判断,如下所示:

    import functools
    def check_is_admin(f):
        @functools.wraps(f)
        def wrapper(*args, **kwargs):
            if kwargs.get('username') != 'admin':
                raise Exception("This user is not allowed to get food")
            return f(*args, **kwargs)
        return wrapper
    
    @check_is_admin
    def get_food(username, food='chocolate'):
        return "{0} get food: {1}".format(username, food)
    
    print get_food('admin')

这段代码看起来没有任何问题,但是,执行将会出错,因为,username是一个位置参数,而不是一个关键字参数。我们在装饰器里面,通过kwargs.get(‘username’)是获取不到username这个变量的。为了保证灵活性,我们可以通过inspect来修改装饰器的代码,如下所示:

    import inspect
    def check_is_admin(f):
        @functools.wraps(f)
        def wrapper(*args, **kwargs):
            func_args = inspect.getcallargs(f, *args, **kwargs)
            if func_args.get('username') != 'admin':
                raise Exception("This user is not allowed to get food")
            return f(*args, **kwargs)
        return wrapper

装饰器还有很多知识,比如装饰器的使用场景,装饰器有哪些缺点,这些你们都知道吗?

2.3 全局变量

关于Python的全局变量,我们先从一个问题开始:Python有没有全局变量?可能你看到这个问题的时候就蒙圈了,没关系,我来解释一下。

从Python自己的角度来说,Python是有全局变量的,所以,Python为我们提供了global关键字,我们能够在函数里面修改全局变量。但是,从C/C++程序员的角度来说,Python是没有全局变量的。因为,Python的全局变量并不是程序级别的(即全局唯一),而是模块级别的。模块就是一个Python文件,是一个独立的、顶层的命名空间。模块内定义的变量,都属于该命名空间下,Python并没有真正的全局变量,变量必然属于某一个模块。

我们来看一个例子,就能够充分理解上面的概念。三种不同的修改全局变量的方法:

    import sys
    
    import test
    
    a = 1
    
    def func1():
        global a
        a += 1
    
    def func2():
        test.a += 1
    
    def func3():
        module = sys.modules['test']
        module.a += 1
    
    func1()
    func2()
    func3()

这段代码虽然看起来都是在对全局变量操作,其实,还涉及到命名空间和模块的工作原理,如果不能很清楚的知道发生了什么,可能需要补充一下自己的知识了。

 

2.4 时间复杂度

我们都知道,在Python里面list是异构元素的集合,并且能够动态增长或收缩,可以通过索引和切片访问。那么,又有多少人知道,list是一个数组而不是一个链表

关于数组和链表的知识,这里就不再赘述。如果我们在写代码的过程中,对于自己最常用的数据结构,连它的时间复杂度都不知道,我们又怎么能够写出高效的代码呢。写不出高效的代码,那我们又怎么能够声称自己精通这门编程语言呢。

既然list是一个数组,那么,我们要使用链表的时候,应该使用什么数据结构呢?在写Python代码的时候,如果你需要一个链表,你应该使用标准库collections中的deque, deque是双向链表。标准库里面有一个queue,看起来和deque有点像,它们是什么关系?这个问题留着读者自己回答。

我们再来看一个很实际的例子:有两个目录,每个目录都有大量文件,求两个目录中都有的文件,此时,用Set比List快很多。因为,Set的底层实现是一个hash表,判断一个元素是否存在于某个集合中,List的时间复杂度为O(n),Set的时间复杂度为O(1),所以这里应该使用Set。我们应该非常清楚Python中各个常用数据结构的时间复杂度,并在实际写代码的过程中,充分利用不同数据结构的优势。

 

2.5 Python中的else

最后我们来看一个对Python语言优缺点理解的例子,即Python中增加的两个else。相对于C++语言或者Java语言,Python语法中多了两个else。

一个在while循环或for循环中:

    while True:
        ....
    else:
        ....

另一个在try…except语句中:

    try:
        ....
    except:
        ....
    else:
        ....
    finally:
       ....

那么,哪一个是好的设计,哪一个是不好的设计呢?要回答这个问题,我们先来看一下在大家固有的观念中,else语句起到什么作用。在所有语言中,else都是和if语句一起出现的:

    if <condition>
        statement1
    else
        statement2

翻译成自然语言就是,如果条件满足,则执行语句1,否则,执行语句2。注意我们前面的用语,是否则,也就是说,else语句在我们固有的观念中,起到的作用是“否则”,是不满足条件的情况下才执行的。

我们来看Python中,while循环后面的else语句。这个else语句是在while语句正常结束的时候执行的。所以,按照语意来说,while循环的else起到的作用是and。也就是说,在Python中,while循环末尾的else换做and才是更加合适的。

你可能觉得我有点钻牛角尖,那好,我再强调一遍,while循环中的else语句是在循环正常结束的时候执行的,那么请问:
1. 如果while循环里面遇到了break语句,else语句会执行吗
2. 如果while循环最后,遇到了continue语句,else语句还会执行吗
3. 如果while循环内部出现异常,else语句还会执行吗

这里的几个问题,大多数人都不能够很快的正确回答出来。而我们的代码是写给人看的,不应该将大多数人排除在能够读懂这段代码之外。所以我认为,Python语言中循环语句末尾的else语句是一个糟糕的设计

现在,我们再来看try…except语句中的else,这个else设计得特别好,其他语言也应该吸取这个设计。这个设计的语义是,执行try里面的语句,这里面的语句可能会出现异常,如果出现了异常,就执行except里面的语句,如果没有出现异常,就执行else里面的语句,最后,无论是否出现异常,都要执行finally语句。这个设计好就好在,else的语句完全和我们的直观感受是一样的,是在没有出现异常的情况下执行。并且,有else比没有else好,有了else以后,正确地将程序员认为可能出现异常的代码和不可能出现异常的代码分开,这样,更加清楚的表明了是哪一条语句可能会出现异常,更多的暴露了程序员的意图,使得代码维护和修改更加容易。

 

三、结论

我这里想说的是,Python是一门编程语言,使用范围非常广泛,大家不要去追求精通Python程序语言自身,而应该将精力放在自己需要解决的实际问题上。其次,绝大多数人对Python的认识都存在误区,认为Python很简单,只是简单地了解一下就开始写Python代码,写出了一堆很不好维护的代码,我希望这一部分人看到我的回答以后,能够回去重新学习Python。最后,对于一些同学的疑虑——招聘职位要求精通Python,我的回答是,他们并不奢望招到一个精通Python的人,他们只是想招到一个合格的工程师,而大部分的Python工程师都不合格!
Categories: Uncategorized Tags:

网易分库分表数据库DDB

December 15th, 2016    阅读(167) No comments

互联网时代,也是关系型数据库独领风骚的时代,从早期的oracle独步天下,到现在MySQL蒸蒸日上,关系型数据库是大多数互联网应用在数据可靠性存储上的“命脉”。

随着互联网产品在体量和规模上的日益膨胀,无论是oracle还是mysql,都会第一时间面临来自磁盘,CPU以及内存等单机瓶颈,为此,产品方除了需要不断购买成本难以控制的高规格服务器,还要面临不断迭代的在线数据迁移。在这种情况下,无论是海量的结构化数据还是快速成长的业务规模,都迫切需要一种水平扩展的方法将存储成本分摊到成本可控的商用服务器上,同时,也希望通过线性扩容降低全量数据迁移对线上服务带来的影响,分库分表方案便应运而生。

分库分表的原理是将数据按照一定的分区规则sharding到不同的关系型数据库中,应用再通过中间件的方式访问各个shard中的数据。分库分表的中间件,隐藏了数据sharding和路由访问的各项细节,使应用大多数场景下可以像使用单机数据库一样使用分库分表后的分布式数据库。业界中,网易DDB,阿里TDDL,corbar,mycat以及hotdb等系统都是分库分表中间件中的佼佼者。

 

背景——十年一剑

DDB(全称Distributed database)是网易杭研院立项最早,应用最为广泛的后台产品之一,也是国内最早出现的数据库分库分表中间件。

最早可以追溯到2006年,网易杭研成立之初,为了应对网易博客这个日活超过800W的大体量应用,由现任杭研院院长的汪源带队主导开发了DDB这套分库分表数据库,伴随着博客的成长,DDB集群也从最早的20+节点,到40+节点,最后到现在云端100+个RDS实例。除了博客外,十年来DDB也见证了很多其他的大体量应用,如易信,云音乐,云阅读,考拉等。在大家耳熟能详的网易互联网产品中,几乎都可以看到DDB的身影。

经过10年的发展和演变,DDB的产品形态已全面趋于成熟,功能和性能得到了众多产品的充分验证,下面罗列一些大家比较关注的功能特性:

  1. 与SQL92标准的兼容度达90%以上
  2. 支持跨库join和跨库事务,支持大部分标量函数
  3. 支持count,sum,avg,max,concat等常用聚合函数
  4. 支持与MySQL高度一致的用户管理
  5. 支持读写分离和数据节点高可用
  6. 支持数据节点在线扩缩容,在线更改表分布
  7. 提供有完善的数据库管理工具,WEB工具以及命令行工具
  8. 数据节点支持oracle和mysql

目前DDB在网易内部有近50个产品使用,最大集群过百数据节点,大部分部署在云端,为应用提供透明,无侵入,MySQL标准协议的分库分表服务。

 

DDB演变之路

十年来,DDB经历了三次服务模式的重大更迭,从最早的Driver模式,到后来的Proxy模式,再到近几年的云模式,DDB服务模式的成长也深刻反映着互联网流行架构的变迁。

Driver模式

Driver模式的特点在于应用通过DDB提供的JDBC Driver来访问DDB,类似与通过MySQL的JDBC驱动访问MySQL。而对于MySQL的驱动connector/J,只需要实现将SQL按照特定协议编码和转码即可。而DDB的驱动为了实现透明的分库分表,需要做很多额外的工作,如下图所示:

%e6%97%a0%e6%a0%87%e9%a2%981

DBI Driver内部模块简图

当DDB D]river执行一条SQL时,会经历以下几个步骤:

  1. 由语法解析器解析SQL,生成抽象语法树parsed tree,并根据是否PreparedStatement决定是否进入PTC(parsed tree cache),PTC保存了SQL模式到语法树的映射,对PreparedStatement SQL,会优先进入PTC中查询语法树
  2. 根据语法树和启发式规则生成分布式执行计划,这个过程中会涉及到多个步骤的SQL转换和优化,如条件合并,join拆分,limit转化等
  3. 由SQL执行器按照执行计划和语法树生成下发给每个数据节点的真实SQL,然后通过标准数据库驱动将SQL下发给各个数据节点,这个过程为并发执行。
  4. 将各个数据节点返回的结果按照执行计划进行合并,并返回上层。具体的合并操作可能在应用调用结果时动态执行。

DBI模块作为DDB提供给应用的JDBC 驱动,包含了完整的透明分库分表逻辑,是DDB最为核心的组件,除此之外,DDB中还有用于元数据管理和同步的master组件,数据库管理工具dbadmin,以及命令行工具isql,DDB的Driver模式整体架构如下图所示:

%e6%97%a0%e6%a0%87%e9%a2%982

DDB Driver模式架构

管理操作以建表为例:

DBA通过DBAdmin的窗口创建表,或者用isql执行建表语句之后,向master发起实际的建表请求,master完成用户认证和合法性校验之后,先在各个数据节点上创建新表,然后将新表元数据记录在系统库中,最后由master将新表元数据同步给各个DBI模块。

对于建表语句中DDB特有的语法,会由isql或dbadmin在解析DDL时完成相应处理,如自增ID的设置。

在DDB中,master用于元数据管理,同步以及报警监控。在DBI模块启动时,会第一时间向master注册,并拉取元数据,之后master对元数据的同步保障了DBI模块元数据的更新。在DBI执行SQL,以及创建DB连接的过程中,不会涉及到与master的交互。

在分库分表中间件中,与DDB Driver模式同样类型的还有阿里TDDL,优势是部署简单,成本较低,容易理解和上手。劣势也非常明显:只支持JAVA客户端,版本难以管理,问题难以追踪,DB连接难以归敛等,另外一点是,中间件与应用绑定在一起,对应用本身是个巨大侵入,而且分库分表的过程是比较耗费CPU资源的,所以在Driver模式下,无论是运维还是性能开销上都存在不可控的因素。

Proxy模式

相比于Driver模式在多语言,版本管理,运维风险上存在的问题,Proxy模式很好地弥补了这些缺陷。所谓Proxy,就是在DDB中搭建了一组代理服务器来提供标准的MySQL服务,在代理服务器内部实现分库分表的逻辑。本质上说,DDB Proxy作为一组独立服务,实现了MySQL标准通信协议,任何语言的MySQL驱动都可以访问,而在Proxy内部,依赖DBI组件实现分库分表,Proxy与DBI的关系如下所示:

%e6%97%a0%e6%a0%87%e9%a2%983

DDB Proxy模块简图

应用通过标准数据库驱动访问DDB Proxy,Proxy内部通过MySQL解码器将请求还原为SQL,并由DDB Driver,也就是DBI模块执行得到结果,最后通过MySQL编码器返回给应用。

从上图可以看出,Proxy在DBI上架设了MySQL编解码模块,从而形成独立标准的MySQL服务,而在MySQL编解码模块之上,DDB Proxy也提供了很多特色命令支持,例如:

  1. show processlist:查看Proxy所有连接状态,与MySQL相关命令高度一致
  2. show connection_pool:查询Proxy到数据节点的连接池状态
  3. show topsql..:查询按照SQL模式聚合的各项统计结果,如执行次数,平均执行时间
  4. . from..:查询过去各个时间段内的吞吐量

此外,DDB Proxy内还提供了slow log等辅助功能,给运维带来很大的便利。

DDB Proxy模式完整架构如下所示:

%e6%97%a0%e6%a0%87%e9%a2%984

DDB Proxy模式架构

与Driver模式架构相比,除了QS(DDBProxy的内部称谓,下同)取代了DBI的位置,还在多个QS节点之上部署了LVS或haproxy + keepalived的组合做负载均衡,从而实现多个DDBProxy节点的热备,由于DDBProxy无状态,或者说状态统一由Master同步,在数据库节点没有达到瓶颈时,可以通过简单地增设QS服务器实现服务线性扩展。

私有云模式

在网易私有云项目启动之前,DDB一直以一个个独立集群为不同业务提供服务,不同DDB各自为政毫不相干,这样的好处是业务之间完全隔离,互不影响,不好之处在于随着使用DDB的产品数目不断增多,一个DBA往往同时运维数个甚至数十个DDB集群,而之前我们一直缺乏一个平台化的管理系统,在DBA在各个集群之间应接不暇时,我们没有平台化的统筹运维帮助应用及早发现问题,或是优化一些使用方法。例如版本管理,2013年我们在一个大版本中做了个hotfix,并通知所有DBA将相关的版本进行升级,但是最后由于管理疏漏,有个别集群没有及时上线,为业务带来了损失。当时如果我们有平台化的管理方案,可以提供一些运维手段帮助和提醒运维人员及时更新所有有问题集群,另外,平台化的管理工具也可以定制一些自动化功能,如自动备份,报警组等。

网易私有云的出现为DDB的思变提供了契机,从12年开始,我们就在基于网易私有云开发一套平台化的管理工具cloudadmin,为此,我们将DDB中原先master的功能打散,一部分分库相关功能集成到Proxy中,如分库管理,表管理,用户管理等,一部分中心化功能集成到cloudadmin中,如报警监控,此外,cloudadmin中提供了一键部署,自动和手动备份,版本管理等平台化功能。私有云DDB的整体架构如下图所示:

%e6%97%a0%e6%a0%87%e9%a2%985

私有云DDB架构

在云DDB解决方案中,还打包了网易私有云LVS服务,cloudadmin通过DDBAgent实现一键部署和报警监控。到目前为止,网易80%以上的DDB集群都已部署云端,云DDB的出现极大减轻了运维人员的负担。

 

DDB特性介绍

分布式执行计划

分布式执行计划定义了SQL在分库分表环境中各个数据库节点上执行的方法,顺序以及合并规则,是DDB实现中最为复杂的一环。

如SQL:select * from user order by id limit 10 offset 10;

这个SQL要查询id排名在10—20之间的user信息,这里涉及到两个合并操作:全局id排序和全局limit offset。对全局id排序,DDB的做法是将id排序下发给各个数据库节点,在DBI层再进行一层归并排序,这样可以充分利用数据库节点的计算资源,同时将中间件层的排序复杂度降到最低,例如一些需要用到临时文件的排序场景,如果在中间件做全排序会导致极大开销。

对全局limit offset,DDB的做法是将offset累加到limit中下发,因为单个数据节点中的offset是没有意义的,且会造成错误的数据偏移,只有在中间件层的全局offset才能保证offset的准确性。

所以最后下发的给各个DBN的SQL变为:select * from user order by id limit 20。

又如SQL:select avg(age) from UserTet group by name

可以通过explain语法得到SQL的执行计划,如下图所示:

%e6%97%a0%e6%a0%87%e9%a2%986

explain示例

上述SQL包含group by分组和avg聚合两种合并操作,与全局order by类似,group by也可以下发给数据节点,中间件层做一个归并去重,但是前提要将group by的字段同时作为order by字段下发,因为归并的前提是排序。对avg聚合,不能直接下发,因为得到所有数据节点各自的平均值,不能求出全局平均值,需要在DBI层把avg转化为sum和count再下发,在结果集合并时再求平均。

DDB执行计划的代价取决于DBI中的排序,过滤和连接,在大部分场景下,排序可以将order by下发来简化为一次性归并排序,这种情况下代价较小,但是对group by和order by同时存在的场景,需要优先下发group by字段的排序,以达到归并分组的目的,这种情况下,就需要将所有元素做一次全排序,除非group by和order by的字段相同。

DDB的连接运算有两种实现,第一种是将连接直接下发,若连接的两张表数据分布完全相同,并且在分区字段上连接,则满足连接直接下发的条件,因为在不同数据节点的分区字段必然没有相同值,不会出现跨库连接的问题。若不满足连接下发条件,会在DBI内部执行nest loop算法,驱动表的顺序与from表排列的次序一致,此时若出现order by表次序与表排列次序不一致,则不满足order by下发条件,也需要在DBI内做一次全排序。

分库分表的执行计划代价相比单机数据库而言,更加难以掌控,即便是相同的SQL模式,在不同的数据分布和分区字段使用方式上,也存在很大的性能差距,DDB的使用要求开发者和DBA对执行计划的原理具有一定认识。

如分库分表在分区字段的使用上很有讲究:一般建议应用中80%以上的SQL查询通过分区字段过滤,使SQL可以单库执行。对于那些没有走分区字段的查询,需要在所有数据节点中并行下发,这对线程和CPU资源是一种极大的消耗,伴随着数据节点的扩展,这种消耗会越来越剧烈。另外,基于分区字段跨库不重合的原理,在分区字段上的分组,聚合,distinct,连接等操作,都可以直接下发,这样对中间件的代价往往是最小的。

分布式事务

分布式事务是个历久弥新的话题,对分库分表,分布式事务的目的是保障分库数据一致性,而跨库事务会遇到各种不可控制的问题,如个别节点永久性宕机,如此像单机事务一样的ACID是无法奢望的。另外,业界著名的CAP理论也告诉我们,对分布式系统,需要将数据一致性和系统可用性,分区容忍性放在天平上一起考虑。

两阶段提交协议(简称2PC)是实现分布式事务较为经典的方案,适用于中间件这种数据节点无耦合的场景。2PC的核心原理是通过提交分阶段和记日志的方式,记录下事务提交所处的阶段状态,在组件宕机重启后,可通过日志恢复事务提交的阶段状态,并在这个状态节点重试,如coordinator重启后,通过日志可以确定提交处于prepare还是prepareAll状态,若是前者,说明有节点可能没有prepare成功,或所有节点prepare成功但是还没有下发commit,状态恢复后给所有节点下发rollback;若是prepareAll状态,需要给所有节点下发commit,数据库节点需要保证commit幂等。与很多其他一致性协议相同,2PC保障的是最终一致性。

2PC整个过程如下图所示:

%e6%97%a0%e6%a0%87%e9%a2%987

2PC过程

在DDB中,DBI和Proxy组件都作为coordinator存在,2PC实现时,记录prepare和prepareAll的日志必须sync,以保障重启后恢复的状态是正确的,而coordinator最后的commit日志主要作用是回收之前日志,可异步执行。

由于2PC要求coordinator记日志,事务吞吐率受到磁盘IO性能的约束,为此DDB实现了group io优化,可极大程度提升2P C的吞吐率。2PC本质上说是一种阻塞式协议,两阶段提交过程需要大量线程资源,因此CPU和磁盘都有额外消耗,与单机事务相比,2PC在响应时间和吞吐率上会相差很多,从CAP的角度出发,可以认为2PC在一定程度上成全了C,牺牲了A。

另外,目前MySQL最流行的5.5和5.6版本中,XA事务日志无法replicate到从节点,这意外着主库一旦宕机,切换到从库后,XA的状态会丢失,可能造成数据不一致,这方面MySQL 5.7已经有所改善。

虽然2PC有诸多不足,我们依然认为在DDB中有实现价值,DDB作为中间件,其迭代周期要比数据库这种底层服务频繁很多,若没有2PC,一次更新或重启就可能造成应用数据不一致。从应用角度看,分布式事务的现实场景常常是无法规避的,在有能力给出其他解决方案前,2PC也是一个不错的选择。

对购物转账等电商和金融业务,中间件层的2PC最大的问题在于业务不可见,一旦出现不可抗力或意想不到的一致性破坏,如数据节点永久性宕机,业务难以根据2PC的日志进行补偿。金融场景下,数据一致性是命根,业务需要对数据有百分之百的掌控力,建议使用TCC这类分布式事务模型,或基于消息队列的柔性事务框架,这两种方案都实现在业务层,业务开发者具有足够掌控力,可以结合SOA框架来架构。原理上说,这两种方案都是大事务拆小事务,小事务变本地事务,最后通过幂等的retry来保障最终一致性。

弹性扩缩容

分库分表数据库中,在线数据迁移也是核心需求,会用在以下两种场景:

  1. 数据节点弹性扩容

随着应用规模不断增长,DDB现有的分库可能有一天不足以支撑更多数据,要求DDB的数据节点具有在线弹性扩容的能力,而新节点加入集群后,按照不同的sharding策略,可能需要将原有一些数据迁入新节点,如hash分区,也有可能不需要在线数据迁移,如一些场景下的range分区。无论如何,具备在线数据迁移是DDB支持弹性扩容的前提。

  1. 数据重分布

开发者在使用DDB过程中,有时会陷入困局,比如一些表的分区字段一开始没考虑清楚,在业务已经初具规模后才明确应该选择其他字段。又如一些表一开始认为数据量很小,单节点分布足以,而随着业务变化,需要转变为多节点sharding。这两种场景都体现了开发者对DDB在线数据迁移功能的潜在需求。

无论是弹性扩容,还是表重分布,都可当做DDB以表或库为单位的一次完整在线数据迁移。可分为两个阶段:全量迁移和增量迁移,全量迁移是将原库或原表中需要迁移的数据dump出来,并使用工具按照分区策略 apply到新库新表中。增量迁移是要将全量迁移过程中产生的增量数据更新按照分区策略apply到新库新表。

全量迁移的方案相对简单,使用DDB自带工具按照特定分区策略dump和load即可。对增量迁移,DDB实现了一套独立的迁移工具hamal来订阅各个数据节点的增量更新,hamal内部又依赖DBI模块将增量更新apply到新库新表,如下图所示:

 %e6%97%a0%e6%a0%87%e9%a2%988

数据迁移工具hamal

Hamal作为独立服务,与Proxy一样由DDB统一配置和管理,每个hamal进程负责一个数据节点的增量迁移,启动时模拟slave向原库拉取binlog存储本地,之后实时的通过DBI模块apply到新库新表,除了基本的迁移功能外,Hamal具备以下两个特性:

  1. 并行复制:hamal的并行复制组件,通过在增量事件之间建立有向无环图,实时判断哪些事件可以并行执行,hamal的并行复制与MySQL的并行复制相比快10倍以上。
  2. 断点续传:hamal的增量apply具有幂等性,在网络中断或进程重启之后可以断点续传

全局表

考虑一种场景:city表记录了国内所有城市信息,应用中有很多业务表需要与city做联表查询,如按照城市分组统计一些业务信息。假设city的主键和分区键都是cityId,若连接操作发生在中间件层,代价较高,为了将连接操作下发数据节点,需要让联接的业务表同样按照cityId分区,而大多数业务表往往不能满足这个条件。

联接直接下发需要满足两个条件,数据分布相同和分区键上联接,除此之外,其实还有一种解法,可以把city表冗余到所有数据节点中,这样各个数据节点本地联接的集合,便是所求结果。DDB将这种类型的表称之为全局表。

全局表的特点是更新极少,通过2PC保障各个节点冗余表的一致性。可以通过在建表语句添加相关 hint指定全局表类型,在应用使用DDB过程中,全局表的概念对应用不可见。

 

未来——独立平台,与云共舞

DDB作为网易浓缩了10年技术经验与精华的分库分表数据库,近一两年除了满足内部产品使用外,也渐渐开始帮助外部企业客户解决海量结构化数据存储的难题。随着公有云技术的大力发展和日趋成熟,各种IaaS和PaaS平台如雨后春笋层出不穷,如网易蜂巢的推出,为应用开发,部署和运维提供了极大便利,而随着IaaS层和PaaS平台的普及,各种SaaS服务也会慢慢为广大开发者所接纳,未来DDB也将重点为网易蜂巢客户打包DDB的SaaS服务,与蜂巢一同构建一套更加丰富的数据存储生态系统。

我们对DDB的SaaS服务化无比坚定,同时DDB的公有云之路绝非私有云的生搬硬套,在与蜂巢一同帮助企业客户解决分库分表难题的同时,未来我们也会更加注重平台独立,首先要做的是将DDB的SaaS层与底层PaaS和IaaS层解耦,实现将DDB平台所依赖的PaaS和IaaS以插件方式注入。这样一来可以为客户提供更灵活的服务方式,二来可以极大程度降低DDB平台本身的开发和运维成本:一套平台管理工具,适用所有内外部DDB用户,这是我们正在进行并将持续优化的目标。

Categories: Uncategorized Tags:

MongoDB的正确使用姿势

December 12th, 2016    阅读(173) No comments

MongoDB是一个非常有前途的数据库,MongoDB官方对自己的定位是通用数据库,其实这个定位跟MySQL有些像。虽其流行度还远未达到MySQL的水平,但笔者有个可能不恰当的比较,MongoDB就像N年前的MySQL,随着时间的推移,会变得越来越强大,也会越来越流行。下面结合MongoDB的几大特色来谈谈MongoDB的适用场景。

首先,MongoDB是文档型(Document store)的NoSQL数据库,数据以文档(对应关系型数据库的记录,本文有时候会混用)的形式在MongoDB中保存,文档实际上就是一个个JSON字符串,想必大家对JSON都比较熟悉,不赘述。使用JSON的好处是非常直观,通过一系列的Key-Value键值对来表示数据,符合我们的阅读习惯,下图所示是以JSON表示的用户信息文档。

在主流的计算机语言如Java、Python中对JSON都有很好的支持,数据从MongoDB中读取出来后,可无需转换直接使用;MongoDB文档另一个特点是Key-Value键值对支持丰富的数据结构,Value可以是普通的整型、字符串,可以是数组,也可以是嵌套的子文档,使用嵌套的好处是在MongoDB中仅需一次简单的查询就能够获取到你所需的数据。举电商领域为例,网易严选上卖的上衣和裤子两种商品,除了有共同属性,如产地、价格、材质、颜色等外,还有各自有不同的属性集,如上衣的独有属性是肩宽、胸围、袖长等,裤子的独有属性是臀围、脚口和裤长等。

这些独有属性可以直接以JSON子文档的方式嵌套在商品这个文档中,一次查询直接获取全部内容,不需要进行多表join;MongoDB文档的另一大特点是模式灵活:不同文档相同key的value类型可以是整形也可以是字符串等其他类型,不同文档可以有不同的key,比如有些商品有折扣字段,可以定义不同会员等级的不同折扣。在电商配套的物流领域,可以将一个快递的物流信息直接嵌套在以商品id为唯一索引的文档中,一次查询就可以获取完整的快递流向信息。MongoDB查询还提供了非常丰富的操作符,在查询中组合使用效率倍增。

基于文档的灵活的数据模式,是MongoDB的一大优势,对于数据模型多样或多变的业务场景,相比MySQL等数据库,无需使用DDL语句进行表结构的修改;相比其他Key-Value数据库,由于MongoDB的Value字段对于MongoDB是非透明的,可以对其建立索引,还可以进行全文检索,在查询效率上更具优势。该模式在游戏、电商、社交、视频直播、物流等领域非常适用,通过在用户或商品中嵌套不同用途的子文档来实现快速查询。对于监控、日志数据存储,第三方信息抓取等场景也同样适用,因为不同监控数据、日志记录、抓取的数据所包含的字段往往是不一样的,某种程度上说也是不可控的。同时,灵活的模式对于类似游戏市场活动、移动App等要求快速开发上线但需求变动(导致数据模型变大)比较大的产品或场景也比较适用。

其次,MongoDB还具有强大的索引能力,支持创建唯一索引、二级索引、TTL索引和地理位置索引等,这在NoSQL数据库中是数一数二的,在此基础上,MongoDB还提供了执行计划功能,通过explain()(https://docs.mongodb.com/manual/reference/method/db.collection.explain/#db.collection.explain)和hint()命令可以查看执行计划、强制查询走某个索引,这些特性相比关系型数据库也不逞多让。MongoDB集合在创建时默认就基于_id字段创建了唯一索引,数据插入时会检查_id字段的唯一性,MongoDB可以在包括数组中字段或嵌套文档中的字段几乎任意字段上创建索引(一般为二级索引),大大提高了查询效率,在没有跨记录或跨表事务但对性能要求又非常高的某些场景下能够替代关系型数据库。在内存足够的情况下,索引会被加载到Cache中,如果执行的查询是索引覆盖的(https://docs.mongodb.com/manual/core/query-optimization/#read-operations-covered-query),其性能甚至可以媲美Redis等内存数据库等。TTL索引在保存日志或监控数据等场景下大有用武之地,通过创建TTL索引,实现自动删除过期记录的功能,(在使用MongoDB TTL索引需要注意,数据的过期时间无法精确控制,无法做到过期即删除,在大数据量的情况下会有一定的性能开销和删除延迟)。

地理位置索引是MongoDB早已被用户所熟知的特性,其支持球面(Spherical)和平面(Flat)两种模式,提供了丰富的地址位置的表示方式,如2d、2dsphere和GeoJSON等,对于移动App,如地图软件、打车软件、外卖软件,MongoDB强大的地理位置索引功能使其最佳选择(https://www.mongodb.com/blog/post/geospatial-performance-improvements-in-mongodb-3-2);此外,对于物联网、智慧都市等领域,也需要大量的地理位置相关操作,这些都是MongoDB的竞技场。

再次,MongoDB的复制集是数据库领域领先的高可用和读写负载均衡解决方案,提供了数据自动(异步/同步)复制能力,一个新节点加入到复制集中会自动进行数据初始同步随后使用oplog进行增量复制,无需人工干预;如果复制集的Primary节点发生宕机,MongoDB会自动进行主从切换,在复制集大多数节点在线的情况下,能够基于Raft协议(MongoDB 3.2开始,之前版本不未使用Raft)自动地快速选出新的Primary并恢复读写服务(在选主期间,无法进行写操作),无需人工干预;MongoDB运维人员所需做的仅仅是将宕机节点重新启动,若宕机的是Primary,则重新启动后,会自动进行数据回滚并最终成为复制集的Secondary节点(正常情况下)。

在复制集机制下,还可以通过对节点进行滚动处理的方式进行在线维护升级。所以,相比目前的大多数关系型数据库,MongoDB复制集实现了自动复制和故障切换,大大减低了运维复杂度,解放了DBA。如果你对数据的持久化和可用性有较高的要求,MongoDB复制集是上佳的选择。此外,复制集还提供了Write Concern、Read Preference、Read Concern和Tag sets等读写行为控制功能,不同的业务应用类型可以参考官方手册(https://docs.mongodb.com/manual/applications/replication/)根据对数据持久化、数据一致性和可用性的不同要求进行灵活地设置。

最后,MongoDB是为大数据而生的,提供sharding机制用于实现业务的水平扩展。每个shard都保存业务的一部分数据,shard可以配置为复制集,确保shard上数据的高可用性,shard内部由一系列连续的chunk组成,chunk是某一片键区间内的数据记录集合;mongos用于业务请求的路由,将业务负载分摊到不同的shard上,此外mongos还会对shard上超过一定大小的chunk进行分裂(split);根据不同shard中数据量的大小,在shard将进行chunk迁移(migrate),应该说sharding提供了完善的业务数据和负载水平扩展的机制,对于物联网、日志系统和监控系统这类包含TB级海量数据的应用场景,使用MongoDB sharding是个不错的选择。

在生产环境中,sharding并不是必须的,并不是新业务起来的时候就马上部署sharding集群,只有当业务的数据量达到单个复制集无法支撑、或者业务的负载超过了复制集的服务能力的时候,才考虑部署sharding,毕竟相比复制集,sharding在部署和管理上都复杂很多。MongoDB复制集可以平滑升级到shard,所以当你真正需要sharding时,可以参考官方文档(https://docs.mongodb.com/manual/tutorial/convert-replica-set-to-replicated-shard-cluster/)进行操作,文档中提供了详细的升级步骤。

介绍了MongoDB的优势,也不得不提MongoDB的不足,MongoDB仅支持文档内的事务,所以对于需要跨文档或跨集合事务的应用,请谨慎使用MongoDB;另外,对于需要多表复杂Join的业务,还是使用关系型数据库为好,MongoDB还在改善的路上;最后,对于PB级大数据量,且需要进行大规模计算的场景,使用MongoDB时需要配套使用Spark、Hadoop等大数据套件,让MongoDB做正确的事情。总结起来,如果你的业务满足一个或多个特点,那么选择MongoDB是个正确的决定:

    • 无需要跨文档或跨表的事务及复杂的join查询支持
    • 敏捷迭代的业务,需求变动频繁,数据模型无法确定
    • 存储的数据格式灵活,不固定,或属于半结构化数据
    • 业务并发访问量大,需数千的QPS
    • TB级以上的海量数据存储,且数据量不断增加
    • 要求存储的数据持久化、不丢失
    • 需要99.999%的数据高可用性
    • 需要大量的地理位置查询、文本查询

目前开源数据库众多,大家可选的余地很大,就会出现这样的问题:MySQL、MongoDB、Redis、Hbase等这些数据库哪个更好?其实这是一个伪命题,脱离了具体的业务场景来讨论好坏是纸上谈兵,没有最好的,只有最合适的,谁也无法保证完全取代谁,上面的每种数据库都在变得更好,都在不停地完善自身。比如MySQL在不断提升其JSON和地理位置处理能力、组复制(group replication)已在开发等;而MongoDB在增强join类型支持,提供更为复杂的多集合查询能力,计划支持事务等;Redis也加入了地理位置处理能力。

        最后,以新鲜出炉的12月份最新DB-Engines排行榜来结束本篇文章:
26aaff2a-2693-4d05-b1e4-a3f5f840faf7
Categories: Uncategorized Tags:

大数据时代快速SQL引擎-Impala

November 21st, 2016    阅读(210) No comments

背景

随着大数据时代的到来,Hadoop在过去几年以接近统治性的方式包揽的ETL和数据分析查询的工作,大家也无意间的想往大数据方向靠拢,即使每天数据也就几十、几百M也要放到Hadoop上作分析,只会适得其反,但是当面对真正的Big Data的时候,Hadoop就会暴露出它对于数据分析查询支持的弱点。甚至出现《MapReduce: 一个巨大的倒退》此类极端的吐槽,这也怪不得Hadoop,毕竟它的设计就是为了批处理,使用用MR的编程模型来实现SQL查询,性能肯定不如意。所以通常我也只是把Hive当做能够提供将SQL语义转换成MR任务的工具,尤其在做ETL的时候。

Dremel论文发表之后,开源社区涌现出了一批基于MPP架构的SQL-on-Hadoop(HDFS)查询引擎,典型代表有Apache Impala、Presto、Apache DrillApache HAWQ等,看上去这些查询引擎提供的功能和实现方式也都大同小异,本文将基于Impala的使用和实现介绍日益发展的基于HDFS的MPP数据查询引擎。

Impala介绍

Apache Impala是由Cloudera开发并开源的一款基于HDFS/Hbase的MPP SQL引擎,它拥有和Hadoop一样的可扩展性、它提供了类SQL(类Hsql)语法,在多用户场景下也能拥有较高的响应速度和吞吐量。它是由Java和C++实现的,Java提供的查询交互的接口和实现,C++实现了查询引擎部分,除此之外,Impala还能够共享Hive Metastore(这逐渐变成一种标准),甚至可以直接使用Hive的JDBC jar和beeline等直接对Impala进行查询、支持丰富的数据存储格式(Parquet、Avro等),当然除了有比较明确的理由,Parquet总是使用Impala的第一选择。

从用户视角

可以将Impala这类系统的用户分为两类,一类是负责数据导入和管理的数据开发同学,另一类则是执行查询的数据分析师同学,前者通常需要将数据存储到HDFS,通过CREATE TABLE的方式创建与数据match的schema,然后通过load data或者add partition的方式将表和数据关联起来,这一些流程串起来还是挺麻烦的,但是多亏了Hive,由于Impala可以共享Hive的MetaStore,这样就可以使用Hive完成此类ETL工作,然后将数据查询的工作交给Impala,大大简化工作流程(据我所知毕竟大部分数据开发同学还是比较熟悉Hive)。接下来对于数据分析师而言就是如何编写正确的SQ以表达他们的查询、分析需求,这也是它们最拿手的了,Impala通常可以在TB级别的数据上提供秒级的查询速度,所以使用起来可能让你从Hive的龟速响应一下提升到期望的速度。

Impala除了支持简单类型之外,还支持String、timestamp、decimal等多种类型,用户还可以对于特殊的逻辑实现自定义函数(UDF)和自定义聚合函数(UDAF),前者可以使用Java和C++实现,后者目前仅支持C++实现,除此之外的schema操作都可以在Hive上实现,由于Impala的存储由HDFS实现,因此不能够实现update、delete语句,如果有此类需求,还是需要重新计算整个分区的数据并且覆盖老数据,这点对于修改的实时性要求比较高的需求还是不能满足的,如果有此类需求还是期待Kudu的支持吧,或者尝试一下传统的MPP数据库,例如GreenPlum。

当完成数据导入之后,用户需要执行COMPUTE STATS <table\>以收集和更新表的统计信息,这些统计信息对于CBO优化器提供数据支持,用于生成更优的物理执行计划。测试发现这个操作的速度还是比较快的,可以将其看做数据导入的一部分,另外需要注意的是这个语句不会自动执行,因此建议用户在load完数据之后手动的执行一次该命令。

系统架构

从用户的使用方式上来看,Impala和Hive还是很相似的,并且可以共享一份元数据,这也大大简化了接入流程,下面我们从实现的角度来看一下Impala是如何工作的。下图展示了Impala的系统架构和查询的执行流程。

Impala系统架构
从上图可以看出,Impala自身包含三个模块:Impalad、Statestore和Catalog,除此之外它还依赖Hive Metastore和HDFS,其中Imapalad负责接受用户的查询请求,也意味着用户的可以将请求发送给任意一个Impalad进程,该进程在本次查询充当协调者(coordinator)的作用,生成执行计划并且分发到其它的Impalad进程执行,最终汇集结果返回给用户,并且对于当前Impalad和其它Impalad进程而言,他们同时也是本次查询的执行者,完成数据读取、物理算子的执行并将结果返回给协调者Impalad。这种无中心查询节点的设计能够最大程度的保证容错性并且很容易做负载均衡。正如图中展示的一样,通常每一个HDFS的DataNode上部署一个Impalad进程,由于HDFS存储数据通常是多副本的,所以这样的部署可以保证数据的本地性,查询尽可能的从本地磁盘读取数据而非网络,从这点可以推断出Impalad对于本地数据的读取应该是通过直接读本地文件的方式,而非调用HDFS的接口。为了实现查询分割的子任务可以做到尽可能的本地数据读取,Impalad需要从Metastore中获取表的数据存储路径,并且从NameNode中获取每一个文件的数据块分布。

Catalog服务提供了元数据的服务,它以单点的形式存在,它既可以从外部系统(例如HDFS NameNode和Hive Metastore)拉取元数据,也负责在Impala中执行的DDL语句提交到Metatstore,由于Impala没有update/delete操作,所以它不需要对HDFS做任何修改。之前我们介绍过有两种方式向Impala中导入数据(DDL)——通过hive或者impala,如果通过hive则改变的是Hive metastore的状态,此时需要通过在Impala中执行REFRESH以通知元数据的更新,而如果在impala中操作则Impalad会将该更新操作通知Catalog,后者通过广播的方式通知其它的Impalad进程。默认情况下Catalog是异步加载元数据的,因此查询可能需要等待元数据加载完成之后才能进行(第一次加载)。该服务的存在将元数据从Impalad进程中独立出来,可以简化Impalad的实现,降低Impalad之间的耦合。

除了Catalog服务,Impala还提供了StateStore服务完成两个工作:消息订阅服务和状态监测功能。Catalog中的元数据就是通过StateStore服务进行广播分发的,它实现了一个Pub-Sub服务,Impalad可以注册它们希望获得的事件类型,Statestore会周期性的发送两种类型的消息给Impalad进程,一种为该Impalad注册监听的事件的更新,基于版本的增量更新(只通知上次成功更新之后的变化)可以减小每次通信的消息大小;另一种消息为心跳信息,StateStore负责统计每一个Impalad进程的状态,Impalad可以据此了解其余Impalad进程的状态,用于判断分配查询任务到哪些节点。由于周期性的推送并且每一个节点的推送频率不一致可能会导致每一个Impalad进程获得的状态不一致,由于每一次查询只依赖于协调者Impalad进程获取的状态进行任务的分配,而不需要多个进程进行再次的协调,因此并不需要保证所有的Impalad状态是一致的。另外,StateStore进程是单点的,并且不会持久化任何数据到磁盘,如果服务挂掉,Impalad则依赖于上一次获得元数据状态进行任务分配,官方并没有提供可靠性部署的方案,通常可以使用DNS方式绑定多个服务以应对单个服务挂掉的情况。

Impalad模块

从Impalad的各个模块可以看出,主要查询处理都是在Impalad进程中完成,StateStore和Catalog帮助Impalad完成元数据的管理和负载监控等工作,其实更进一步可以将Query Planner和Query Coordinator模块从Impalad移出单独的作为一个入口服务存在,而Impalad仅负责数据读写和子任务的执行。

在Impalad进行执行优化的时候根本原则是尽可能的数据本地读取,减少网络通信,毕竟在不考虑内存缓存数据的情况下,从远端读取数据需要磁盘->内存->网卡->本地网卡->本地内存的过程,而从本地读取数据仅需要本地磁盘->本地内存的过程,可以看出,在相同的硬件结构下,读取其他节点数据始终本地磁盘的数据读取速度。

Impalad服务由三个模块组成:Query Planner、Query Coordinator和Query Executor,前两个模块组成前端,负责接收SQL查询请求,解析SQL并转换成执行计划,交由后端执行,语法方面它既支持基本的操作(select、project、join、group by、filter、order by、limit等),也支持关联子查询和非关联子查询,支持各种outer-join和窗口函数,这部分按照通用的解析流程分为查询解析->语法分析->查询优化,最终生成物理执行计划。对于Query Planner而言,它生成物理执行计划的过程分成两步,首先生成单节点执行计划,然后再根据它得到分区可并行的执行计划。前者是根据类似于RDBMS进行执行优化的过程,决定join顺序,对join执行谓词下推,根据关系运算公式进行一些转换等,这个执行计划的生成过程依赖于Impala表和分区的统计信息。第二步是根据上一步生成的单节点执行计划得到分布式执行计划,可参照Dremel的执行过程。在上一步已经决定了join的顺序,这一步需要决定join的策略:使用hash join还是broadcast join,前者一般针对两个大表,根据join键进行hash分区以使得相同的id散列到相同的节点上进行join,后者通过广播整个小表到所有节点,Impala选择的策略是依赖于网络通信的最小化。对于聚合操作,通常需要首先在每个节点上执行预聚合,然后再根据聚合键的值进行hash将结果散列到多个节点再进行一次merge,最终在coordinator节点上进行最终的合并(只需要合并就可以了),当然对于非group by的聚合运算,则可以将每一个节点预聚合的结果交给一个节点进行merge。sort和top-n的运算和这个类似。

下图展示了执行select t1.n1, t2.n2, count(1) as c from t1 join t2 on t1.id = t2.id join t3 on t1.id = t3.id where t3.n3 between ‘a’ and ‘f’ group by t1.n1, t2.n2 order by c desc limit 100;查询的执行逻辑,首先Query Planner生成单机的物理执行计划,如下图所示:

单机执行计划
和大多数数据库实现一样,第一步生成了一个单节点的执行计划,利用Parquet等列式存储,可以在SCAN操作的时候只读取需要的列,并且可以将谓词下推到SCAN中,大大降低数据读取。然后执行join、aggregation、sort和limit等操作,这样的执行计划需要再转换成分布式执行计划,如下图。

分布式执行计划
这类的查询执行流程类似于Dremel,首先根据三个表的大小权衡使用的join方式,这里T1和T2使用hash join,此时需要按照id的值分别将T1和T2分散到不同的Impalad进程,但是相同的id会散列到相同的Impalad进程,这样每一个join之后是全部数据的一部分。对于T3的join使用boardcast的方式,每一个节点都会收到T3的全部数据(只需要id列),在执行完join之后可以根据group by执行本地的预聚合,每一个节点的预聚合结果只是最终结果的一部分(不同的节点可能存在相同的group by的值),需要再进行一次全局的聚合,而全局的聚合同样需要并行,则根据聚合列进行hash分散到不同的节点执行merge运算(其实仍然是一次聚合运算),一般情况下为了较少数据的网络传输, intermediate节点同样也是worker节点。通过本次的聚合,相同的key只存在于一个节点,然后对于每一个节点进行排序和TopN计算,最终将每一个Worker的结果返回给coordinator进行合并、排序、limit计算,返回结果给用户。

Impalad优化

上面介绍了整个查询大致的执行流程,Impalad的后端使用的是C++实现的,这使得它可以针对硬件做一些特殊的优化,并且可以比使用JAVA实现的SQL引擎有更好的资源使用率。另外,后端的实现使用了LLVM,它是一个编译器框架,可以在执行器生成并编译代码。官方测试发现使用动态生成代码机制可以使得后端执行性能提高1—5倍。

在数据访问方面,Impalad并没有使用通用的HDFS读取数据那一套流程,毕竟Impalad一般部署在DataNode上,访问数据完全不需要再走NameNode了,因此它使用了HDFS提供的Short-Circuit Local Reads机制,它提供了直接访问DataNode的方案,可以参考Hadoop官方文档HDFS-347了解详情。

最后Impalad后端支持对中文件格式和压缩数据的读取,包括Avro、RC、Sequence、Parquet,支持snappy、gzip、bz2等压缩,看来Impala不支持可能也不打算支持ORC格式啦,毕竟有自家主推的Parquet,而ORC则在Presto中广泛使用。关于Parquet和ORC等列式存储格式可参考这里这里,还有这里

部署方式

通常情况下,我们会考虑两种方式的集群部署:混合部署和独立部署,下图分别展示了混合部署与独立部署时的各节点结构。混合部署意味着将Impala集群部署在Hadoop集群之上,共享整个Hadoop集群的资源;独立部署则是单独使用部分机器只部署HDFS和Impala,前者的优势是Impala可以和Hadoop集群共享数据,不需要进行数据的拷贝,但是存在Impala和Hadoop集群抢占资源的情况,进而可能影响Impala的查询性能(MR任务也可能被Impala影响),而后者可以提供稳定的高性能,但是需要持续的从Hadoop集群拷贝数据到Impala集群上,增加了ETL的复杂度。两种方式各有优劣,但是针对前一种部署方案,需要考虑如何分配资源的问题,首先在混合部署的情况下不可能再让Impalad进程常驻(这样相当于把每一个NodeManager的资源分出去了一部分,并且不能充分利用集群资源),但是YARN的资源分配机制延迟太大,对于Impala的查询速度有很大的影响,于是Impala很早就设计了一种在YARN上完成Impala资源调度的方案——Llama(Low Latency Application MAster),它其实是一个AM的角色,对于Impala而言。它的要求是在查询执行之前必须确保需要的资源可用,否则可能出现一个Impalad的阻塞而影响整个查询的响应速度(木桶原理),Llama会在Impala查询之前申请足够的资源,并且在查询完成之后尽可能的缓存资源,只有当YARN需要将该部分资源用于其它工作时,Llama才会将资源释放。虽然Llama尽可能的保持资源,但是当混合部署的情况下,还是可能存在Impala查询获取不到资源的情况,所以为了保证高性能,还是建议独立部署。

两种不同的部署方式

测试

我们小组的同事对Impala做了一次基于TPCDS数据集的性能测试,分别基于1TB和10TB的数据集,可以看出,它的查询性能较之于Hive有数量级级别的提升,对比Spark SQL也有几倍的提升,Compute stat操作可以给Impala带来一定的查询优化,但是偶尔反而误导查询优化器以至于性能下降,最后我们还测试了Impala on Kudu,发现它并没有达到意料中的性能(几倍的差别)。唯一的缺憾是我们并没有对多用户并发场景下进行测试,不过从单个查询的资源消耗来看,C++实现的Impala对资源的消耗也是最少的,可以推断出在多用户下它仍然能满足快速响应的需求,最后是官方给出的多用户场景下的对比结果(有点故意黑Presto的感觉)。

1TB数据集测试结果
1TB数据集与spark对比测试结果
10TB数据集测试结果
10TB数据集与spark对比测试结果
parquet与kudu对比测试
Impala on parquet与Impala on Kudu对比测试结果
并发测试结果
并发测试结果

总结

本文主要介绍了Impala这个高性能的ad-hoc查询引擎,分别从使用、原理和部署等方面做了详细的分析,最终基于我们的测试结果也证实了它的高性能,区别于传统DBMS的MPP解决方案,例如Greenplum、Vertica、Teradata等,Impala更好的融入大数据(Hadoop/Spark)生态圈,更好的实现数据之间的流通,而传统MPP数据库,更倾向于数据自制。当然基于HDFS的实现导致Impala无法实现单条数据的实时更新,而只能批量的追加或者覆盖数据,虽然Cloudera也提供了Impala对于Kudu的支持,但是从性能测试结果看,目前查询性能还是不理想,而传统MPP数据库不仅可以支持单条数据的实时更新,甚至能够在保证查询性能的情况下支持较复杂的事务,这也是SQL-on-Hadoop查询引擎所望尘莫及的。但是无论如何,这类的查询引擎毕竟支持SQL引擎而不是一个完整的数据库系统,它提供给用户在大数据圈中高性能的查询服务,这也能够满足了大部分用户的需求。

参考

Impala: A Modern, Open-Source SQL Engine for Hadoop

Dremel: interactive analysis of web-scale datasets

Impala原理及其调优

Impala:新一代开源大数据分析引擎

Apache Impala Documents

Categories: Uncategorized Tags:

Kylin性能调优记——业务技术两手抓

November 14th, 2016    阅读(254) No comments

背景

最近开始使用了新版本的Kylin,在此之前对于新版本的了解只是代码实现和一些简单的新功能测试,但是并没有导入实际场景的数据做分析和查询,线上Hadoop稳定之后,逐渐得将一些老需求往新的环境迁移,基于以前的调研,新版本(V2,版本为1.5.2)的Kylin提供了几个比较显著的功能和优化:

  • 新的度量类型,包括TOPN、基于bitmap的精确distinct count和RAW。
  • 自定义度量框架,用户可以定义一些特殊的度量需求。
  • Fast Cubing算法,减少MR任务,提升build性能。
  • 查询优化,Endpoint Coprocessor,并行scan和数据压缩,这部分对于查询性能提升还是比较显著的。
  • shard hbase存储,基于一致性哈希的rawkey分布,尽可能的将大的cuboid分散到不同的region上,增加并行扫描度。
  • spark计算引擎,in memory准实时计算,这两项目前还处于试验阶段。
  • 新的aggregation group分区算法。

有了这么多新的特性和性能提升,经常拿新版本来应付用户的需求:新版本实现了xxx功能,肯定性能会很好,等到新版本稳定了再来搞这个需求吧。等到我们的新版本上线了,业务需求真正上来之后,才发现用起来没有相当的那么简单,对于Kylin这种直接面向用户需求的服务,对于一些特殊的需求更是要不断打磨和调整才能达到更好的性能。本文整理在接入云音乐一个较为复杂的需求的实现和中间调优的过程,一方面开拓一下自己的思路,另外也使得读者能改更好的使用Kylin。

业务需求

数据通过日志导入到Hive中查询,经过一定的聚合运算,整理成如图1中的格式。这个数据源是针对歌曲统计的,每一首歌曲包含歌曲属性信息(歌曲名、歌手ID,专辑ID等)、支付方式、所属圈子ID、标签信息等,需要统计的指标包括每一首歌曲的各种操作的PV和UV,操作包括播放、收藏、下载等10种。因此一共20左右个指标,然后需要按照这20个指标的任意一个指标计算TOP N歌曲和其他统计信息(N可能是1000,1W等),除此之外,在计算TOP N的时候还需要支持字段的过滤,可以执行过滤的字段包括支付方式、圈子ID、标签等。歌曲ID全部的成员数大概在千万级别,该表的数据每天导入到hive,经过初步的聚合计算生成图1中的格式,基本上保证了每天的每一个用户对于每一首歌曲只保留一条记录,每天的记录数大概在几亿条(聚合之后),查询通常只需要查询不同条件下的TOPN的song_id。查询性能尽可能的高(秒级),需要提供给分析人员使用。

简单聚合的数据

基于kylin 1.x版本的实现

Kylin 1.3.0之前的版本对于这种需求的确有点无能为力,我们使用了最简单的方式实现,创建一个包含所有维度、所有度量的cube,其中10个度量为count distinct,使用hyperloglog实现的近似去重计数算法,这种做法能够可以说是最原始的,每次查询时如果不进行任何过滤则需要从hbase中扫描几百万条记录才能满足TOPN的查询,每一条记录还都包含hyperloglog序列化的值(读取到内存之后还需要对hyperloglog进行反序列化),TOP的查询速度可想而知,正常情况(没有并发)下能够在1、2分钟出结果,偶尔还会有一个查询把服务搞挂(顺便吐槽一下我们使用对的虚拟机)。查询以天的数据就这种情况了,对于跨天的数据更不敢尝试了。

这种状况肯定无法满足需求方,只能等着新版本落地之后使用TOPN度量。

需求简化

面对这样的现实,感觉对于去重计数无能为力了,尤其像这种包含10个distinct count度量的cube,需求方也意识到这种困境,对需求做出了一定的让步:不需要跨天计算TOPN的song_id了,只需要根据每一天的PV或者UV值计算TOPN,这样就不需要distinct count了,而且还能保证每天的统计值是精确的。如果希望计算大的时间周期的TOPN,则通过创建一个新的cube实现每个自然周、自然月的统计。对于这样的需求简化,可以大大提升cube的创建,首先需要对数据源做一些改变(这种改变通常是view来完成),将原始表按照歌曲ID、用户ID和其它的维度进行聚合(group by,保证转换后的新表每一个用户对于每一首歌曲只保存一条记录),PV则直接根据计算SUM(操作次数),UV通过表达式if(SUM(操作次数) > 0, 1, 0)确定每一个userid对每一个songid在当天是否进行了某种操作,这样我们就得到如图2中格式的表:

歌曲和用户进行聚合

这个表中可以保证歌曲ID+用户ID是唯一的(其它的维度是歌曲的属性,一个歌曲只对应相同的属性),对于Kylin来说原始数据量也变小了(还是每天几亿条记录),接下来新建的Cube由于数据源保证每一条记录不是同一个用户对同一首歌曲的操作,因此所有的度量都可以用SUM来实现(去重计数也可以通过对sum(是否xxx)统计精确的计数值),这样对于Kylin的负担减少了一大部分,但是扫描记录数还是这么多(Cube中最大的维度为歌曲,每次查询都需要携带,查询时扫描记录数始终保持在歌曲ID的个数),但是Build性能提升了,查询性能也由于扫描数据量减少(一个SUM度量占用的存储空间只有8字节)而提升,但是还是需要30+秒。

此时,新版本终于在公司内部落地了,仿佛看到了新的曙光。

Kylin新版本(2.x)的实现

新版本有了TOPN度量了,但是目前只支持最大TOP 1000,可以通过直接改cube的json定义改变成最大得到5000,于是尝试了几种使用TOPN的方案。

第一种是将所有维度放在一个Cube中然后对于每一个PV和UV创建一个TOPN度量。这样build速度还是挺快的,但是查询时大约需要25秒+,最坑爹的是,创建TOPN时需要指定一个指标列(做SUM,根据该列的SUM值排序)和一个聚合列(歌曲ID),使用一个聚合列创建的多个TOPN度量居然只有查询第一个度量时才有结果,使用其他指标查询TOPN时返回的聚合值总是null,难道我要为每一个指标都建一个cube,每一个只包含一个topn度量?

第二种方案是不使用全部维度,而只使用在计算TOPN时需要做过滤的列,例如支付类型、圈子ID等维度建包含TOPN的Cube,这样通过抽取部分维度创建一个小Cube用来计算TOPN,然后再使用计算出来的一批批歌曲ID从大Cube(包含全部维度的cube)中取出这个ID对应的其它指标和属性信息。这样做的弊端是对于查询不友好,查询时需要执行两条SQL并且在前端进行分页,如果性能可以接受也就认了,但是现实总是那么残酷,当创建了一个这样的Cube(包含歌曲ID和其他几个成员值很少的维度)进行build数据时,让人意想不到的事情发生了,计算第二层Cuboid的任务居然跑了3个小时才勉强跑完,接下来的任务就更不敢想象了,MR任务的task使用CPU总是100%,通过单机的测试发现多个TOPN的值进行merge的性能非常差,尤其是N比较大的时候(而且Kylin内部会把N放大50倍以减少误差),看来这个方案连测试查询性能的机会都没有了。

尝试了这两个方案之后我开始慢慢的失去信心了,但是想到了Kylin新版本的并行扫描会对性能有所提升,于是就创建了一个小cube(包含歌曲ID和几个过滤维度,和全部的SUM度量)用于计算TOPN,整个cube计算一天的数据只需要不到1G的存储,想必扫描性能会更好吧,查询测试发现在不加任何过滤条件的情况下对任意指标的TOPN查询大约不到15s,但是扫描的记录数仍然是几百W(歌曲ID的数目),可见并行扫描带来的性能提升,但是扫描记录数仍然是一个绕不过去的坎。

在经历这么多打击之后,实在有点不知所措了,感觉歌曲ID的数目始终是限制查询性能的最大障碍,毕竟要想计算TOPN肯定要查看每一个歌曲ID的统计值,然后再排序,避免不了这么多的扫描,唯一的方法也就是减小region的大小,使得一天的数据分布在更多的region中,增大并行度,但这也不是最终的解决方案啊,怎么减小扫描记录数呢?

从业务出发

陷入了上面的思考之后,提升查询性能只有通过减少扫描记录数实现,而记录数最少也要等于歌曲ID的成员数,是否可以减少歌曲ID的成员数呢?需求方的一个提醒启发了我们,全部的20个指标最关键的是播放次数,一般播放次数在TOPN的歌曲,也很有可能在其他指标的TOPN中。那么是否可以通过去除长尾数据来减少歌曲ID的个数呢?首先查了一下每天播放次数大于50的个数数量,查询结果对于没接触过业务的我来说还是很吃惊的,居然还剩几十W,也就意味着通过排除每天播放次数小于50的歌曲,可以减少将近80%+的歌曲ID,而这些歌曲ID对于任何指标的TOPN查询都是没有任何作用的,于是通过view把数据源进行在过滤和聚合,创建包含全部维度的cube,查询速度果然杠杠的,只需要2s左右了!毕竟只需要扫描的记录数只有几十万,并且都是SUM的度量。

终于达成目标了,但是这种查询其实是对原始数据的过滤,万一需求方需要查询这些播放次数比较少的歌曲ID的其他指标呢?完全不能从Kylin中获取啊。此时,业务方的哥们根据数据特性想到了一个很好的办法:为播放次数建索引!根据播放次数的数值创建一个新的维度,这个维度的值等于播放次数所在的区间,例如播放次数是几位数这个维度的值就是多少,或者使用case when sum(播放次数)自定义不同的范围。例如某一个歌曲在当天SUM(播放次数)为千万级,那么这个歌曲对应的index维度值为8,百万级则值为7,以此类推。然后查询的时候根据index过滤确定播放次数的范围,这样可以大大减少歌曲ID的数目,进而减少扫描记录数。

此时需要对图2中的记录格式再进行整理,通过group by 歌曲ID,xxx(其他维度)统计出每一首歌曲的操作次数和操作人数,然后再根据操作次数的值确定index的值。得到新的记录格式如图3.

说干就干,整理完数据之后再添加一个index维度创建新的Cube,查询的时候可以先查询select 歌曲ID, sum(指标) as c from table where index >= 7 group by 歌曲ID order by c desc limit 100,使用着这种查询可以直接过滤掉所有播放次数小于百万的歌曲ID,但是经过测试发现,这种查询时间还是15s左右,和上面的方案性能并无改善。整得我们开始怀疑人生了。

聚合并加index

回归技术

接下来感觉是走投无路之后的各种猜测,例如猜测shard by设置true or false是否对性能有很大的影响,经过查看源码发现shard by只是为了决定该列的值是否参与一致性哈希中桶号的计算,只不过是为了更好的打散rowkey的分布,对于存储和查询性能提升不是太明显,思来想去还是不合理,既然加了过滤不应该还要扫描全部的歌曲ID,是不是Cube中rowkey的顺序错了,查了一下cube的定义,发现index维度被放在rowkey的最后一行,这显然是不了解Kylin是如何确定扫描范围的!Kylin存储在hbase中的记录是rowkey是根据定义Cube时确定的rowkey顺序确定的,把如果查询的时候对某一个维度进行范围过滤,Kylin则会根据过滤的范围确定扫描的区间以减小扫描记录数,如果把这个维度放到rowkey的最后,则将这种过滤带来的减小扫描区间的作用降到了最低。

重新修改Cube把index维度在rowkey中的位置提升到最前面之后重新build,此后再执行相同的查询速度有了质的飞跃,降低到了1s以内。根据index的范围不同扫描记录数也随着改变,但是对于查询TOPN一般只需要使用index >= 6的过滤条件,这种条件下扫描记录数可以降低到几万,并且保证了全部歌曲ID可以被查询。

总结

本文记录了云音乐的一个比较常见的TOPN的需求的优化,在整个优化的过程中在技术和业务上都给与了充分的考虑,在满足查询需求的前提下,查询性能从最初的上百秒提升到1秒以内,这个过程让我充分意识到了解数据特征和业务的重要性,以前遇到问题总是想着是否能够进行算法的优化,是否可以加机器解决,但是当这些都不能再有改进的空间时,转换一下思路,思考一下是否可以从业务数据入手进行优化,可能会取得更好的效果。

随着Kylin 2.x的落地使用,对于业务的支持也更有信心了,尤其是并行查询和Endpoint Coprocessor对性能的提升,但是对于开源技术来说,如何正确的使用才是最困难的问题,我们也会在探索Kylin使用的路上越走越远。

最后,通过本次调优,我发现了一个以前没有注意到的问题:薛之谦最近真的挺火,不相信的话可以查看一下网易云音乐热歌榜。

Categories: Uncategorized Tags:

大数据平台的服务内容以及猛犸大数据平台近期的思考

November 8th, 2016    阅读(165) No comments

猛犸大数据平台经过去年一年的快速发展,已成为公司内多个产品的大数据开发工具的首选,作为一个当初定位为开发门户的这样一个平台网站,以调度管理为核心,将公司内已有的大数据工具进行了整合,提供了可视化的操作界面、统一的用户权限管理机制。洞悉原油开发流程的用户可以在猛犸上找到很熟悉的感觉,DS接入,MR任务的上传与调度控制,HIVE的查询等等。随着用户不断反馈,猛犸也在不断的进化,越来越多的组件涵盖了进来,交互和流程在不断改善。然而目前这样的框架这就是猛犸的终极形态吗?

答案自然是否定的,可以说,眼前的猛犸只是窥探到了大数据生态的冰山一角,组件的堆积无法成为真正的生态系统,只是工具的累积。只有将各个组件有机的结合在一起,形成完整的工作流方案,同时辅以更靠近业务的公共服务,才能使得平台在整个大数据开发使用流程上发挥更大的作用。

本文主要从平台的服务对象、核心、服务形态等几个不同的方面来对理想的大数据平台进行探讨。

细分服务对象

大数据平台真正应该服务的对象是哪些人,这是个人在接手项目之初就在不断思考的问题。

Alt pic

猛犸最早的名称是“猛犸大数据开发平台”,这也意味着从最早的版本开始,猛犸的主要目标就是服务公司内各个项目的数据开发人员。数据开发的工作在公司内主要是ETL(Extract-Transform-Load),细的来说就是同步各个数据源,利用MR或者Hive做数据的抽取转换,结合调度系统维护一整套数据流的运转。数据开发可以说是整个数据体系的核心。他们是数据的加工者,在客观的数据和最终的数据应用之间架起了一座桥梁。

然而随着猛犸上用户越来越多,平台与底层系统间的依赖越来越重,平台管理员也是猛犸中重要的一类用户。猛犸平台另外一个目标是作为整个大数据平台的唯一入口,所有的任务和调度都通过猛犸提交,由猛犸来接管审计权限之类的管理。随之而来的一些hadoop管理员、DBA等等平台管理角色也有很强的意愿在猛犸中加入管理以及统计查询功能,能够方便的进行一些运维操作以及查看系统的负载,任务提交的状况等等。

除了数据开发、系统管理员之外还有第三类用户,也是目前猛犸尚未照顾到的用户——数据使用者,随着数据在产品运营中所占的比重越来越大。越来越多的人开始使用数据,包括数据分析师以及普通的运营决策人员。具有一定数据常识的人如果能自助化的获取一些简单的数据,将大大解放数据开发人员的压力。现今过往那种数据开发人员统一处理所有数据获取请求的模式是不合理,也是不可扩展的。在这种模式下,数据响应会变得非常缓慢,数据开发也很容易成为瓶颈,他们宝贵的时间容易被各种业务所淹没,而真正的数据建设任务反而容易被搁置起来。

服务细分,这是未来的趋向,不通的用户的需求是不尽相同的,如何让所有的角色都感到方便易用是平台急切需要考虑的问题。根据不同分类的用户来划分平台的功能模块是一个可行的方法,猛犸在最新的版本中将数据开发和数据使用者进行了划分,通过不同的入口进行引导,

数据平台的核心

大数据平台真正的核心价值是什么,是基于底层系统的一套交互UI吗?当然不是。 是任务调度?仅仅是如此吗?究竟什么才是一个大数据平台真正的核心?

数据仓库,是的,我认为数据仓库是数据平台真正的核心。这里所指的数据仓库是一个广泛的定义,应该这么说,数据仓库是用来保存从多个数据库及其它来源的数据,经过转换处理并为上层应用提供统一的用户接口完成数据的查询以及分析的一套体系。Inmon对数仓的定义:数据仓库是一个面向主题的、集成的、时变的和非易失的数据集合,支持管理部门的决策过程。

数据仓库是一个数据管理的范畴,围绕数据仓库这个中心,在数据开发层面最重要的流程就是ETL了,关于ETL以下的几点总是绕不开,却又很容易被忽视的话题。

元数据管理

首先就是元数据的管理。元数据的作用体现在这几个方面,首先元数据是数据集成的必需品,其次元数据能帮助用户理解数据,再其次元数据是保证数据质量的关键,能帮助用户了解数据的来龙去脉。最后元数据是支持数据需求变化的基础。具体来讲,需要元数据管理系统来管理这些对数据的描述,包括技术层面的元信息以及业务层面的元信息。例如纪录数据仓库的分层模型,哪些表哪些数据是属于明细层,哪些是属于聚合层。每张表的每个字段分别对应的是什么含义,格式标准是什么,多维分析模型中哪些是事实表,哪些是维度表,维度的层次、级别、属性,度量的定义、筛选条件。数据流的情况,数据的血缘关系等等都是必须要追踪到的。在这些元数据的基础之上需要有一套快速检索的机制,帮助不熟悉数据仓库的使用者快速定位到想要获取的数据。

做好一套元数据管理系统有以下几件事必须要完成:

  1. 完整的数据字典repository
  2. 数据血缘关系

数据仓库中包含着原始数据、中间过程数据、宽表数据、集市数据等等。一个合理的数据仓库必须做到快速准确的追踪和管理所属的数据,对数据按主题,按更新周期,按照粒度级别进行区分,或者说以较高的层次对数据进行完整、一致性的描述。 过往,数据开发的同学往往是用Wiki或者是git等工具记录对于数据表、数据文件的描述。这样做有两个问题,首先是描述的格式比较自由,数据没法根据数据仓库元数据的要求进行统一描述;其次,这份元数据很难得到及时的更新,一旦有其他使用者在数据仓库中对元数据进行了修改,很难及时可靠的反应出来,最终会导致元数据的滞后、失效以及混乱。

对于猛犸来说,目前最新的版本已经加入了部分数据管理的功能。对于帮助用户理解数据这一层,猛犸对表,对表字段都提供了纪录、搜索、收藏等功能,猛犸可以对Hive中已存在的表进行关联,数据开发同学可以将Hive表关联至数据字典Repository中,对这些数据进行分类、tag以及进一步的描述和说明。普通数据用户能够快速的进行数据字典的检索,获取对自己有帮助的数据表、数据列。未来,猛犸也会附带上数据血缘关系的功能,数据血缘是在数据开发的基础上对数据做的纵向追踪。血缘关系的核心是任务调度系统,根据任务调度系统的每个Job获取输入输出、执行日志等等。将项目中每个任务的输入输出进行关联组织成一个有向无环图(DAG),DAG的节点就是输入输出的文件,DAG的边就是一个个注册在调度系统中的JOB。 从实现来说,MR Job 或者 Spark RDD中都包含有血缘的关系,Hive也有专门的 org.apache.hadoop.hive.ql.tools.LineageInfo 工具能够获取上下文的输入输出表。

数据质量管控

除了元数据管理之外,数据质量的监控也是一个非常重要的方面, 数据的质量可以由完整性、一致性、准确性和及时性4个基本要素来构成。包括了Profiling,Auditing,Correcting三个重要的流程:

profiling:数据的概要分析, 检查数据是否可用,并收集数据的统计和其他信息。 类似于传统数据库中的Analyze, 比较全面的profiling包括收集记录数、最大最小值、最大最小长度,cardinality,null个数,平均数、中位数,唯一值的分布信息等等。从一些关键的量化指标中获取数据的特征,捕捉潜在的异常点, 部分工具甚至可以给出数据质量的评分。

Auditing:数据审核,基于数据质量的4个基本方面进行校验,及时性方面比较直接,是通过对ETL任务的监控来完成,完整性包括字段完整性和记录完整性。字段完整性最常见的异常就是在统计信息中空值的数量过多,记录完整版的常见异常包括记录数量过多、过少等反常情况。一致性包括编码规则一致以及逻辑规则一致,编码规则可以基于既定规则进行判断,而数据逻辑一致性相对复杂,有属性内的规则,也有属性间的规则。准确性的问题一般是数据数量级、字符乱码、字符截断之类的错误,可以通过该要分析的中位数、平均值以及数据分布来发现异常,

correcting:数据修正,包括填补缺失值、删除重复记录、一致化数据以及修正异常数据。 对于记录缺失,需要重新从原始数据获取,字段的缺失需要对缺失的值进行预测或者估计,去重需要根据唯一值的约束去判断,不一致记录就需要依赖对数据源的熟悉度,指定一枝花的规则。总的来说大部分的异常数据是很难修正的,很多异常数据是不可能百分百进行还原。最后的手段是对异常数据进行吧过滤,将异常的数据排除出数据仓库,避免干扰。

猛犸在数据质量管理方面目前只有ETL任务的监控,猛犸可以对ETL的各个Task,Job甚至是对Hadoop的Job进行监控,对于失败或者超时的任务进行报警。但是在一致性、完整性、准确性方面是空白的,这一块也是猛犸下一阶段的发力重点。考虑到数据correcting的主观性以及不确定性,猛犸的数据质量保证着重于profiling以及audition,希望能尽早的帮助用户发现数据的异常,让用户尽早开始补救措施。猛犸的数据质量保证是一个插件化的模块,允许可选式的集成在任务调度系统中。对于数据重要性高,计算资源相对宽裕的用户,数据质量保证能够很好的提高服务体验,保证数据仓库体系的正常运转。

数据开发模式的变化

在传统的数据处理、数据集成、ETL或者是ELT开发中,有专业的数据公司提供专业的辅助工具来完成,比如Oracle的OWB,MS的SSIS等等。而在大数据时代,以hadoop作为核心数据仓库的时候,并没有合适的开源ETL工具来辅助作业。我们能看到大量的数据开发同学手写MapReduce来完成从数据清洗、数据转换等等一系列。要维护这一套代码以及使之正常运转是一项大工程,这好比在现代的程序开发中,开发者维护一套汇编代码一样!

手写MapReduce的短板是显而易见的,作为底层的API其复杂度较高,相对的自由度也比较高。在很高的自由度下,代码的规范以及可读性、可维护性就成为了一个问题。虽然MapReduce的代码想对来说在优化的情况下性能比较好,但是相比可读性、可维护性来说,这些许的性能优势并不是最为重要的。在实际的项目中常常能看到在工作交接中,一堆的MR工程代码是非常难维护的,接手的同事在修改的时候畏手畏脚。好不容易接手的代码扩展它又想对比较麻烦,开发效率非常低下。

为了解决这样的问题,社区有了各种各样的工具。Hive,Cascading等等,Hive能使用SQL语言来概括复杂的MR任务,辅以Hive可扩展的UDF、UDAF,可以说SQL作为一种更简练、概括性更高的语言能承担所有的ETL开发任务,维护SQL可以有效的解决手写MapReduce的不足,而且作为受众面非常广的SQL,入门门槛进一步降低,可以让数据开发更多的集中于处理业务,而不是疲于维护一堆底层代码。

猛犸也将尝试提供一系列的SQL化ETL工具,希望这种便捷的开发方式能帮助开发更快的入门进行数据仓库的建设。这样的改造将从数据入库开始,将“数据结构化“ 这个核心思路贯穿整个开发流程。在Hadoop中数据流转的单位将从HDFS文件变成一张张二维表,这也能够和传统的以关系数据库作为数据仓库基础的ETL流程更好的衔接起来。在新的开发模式下,猛犸需要努力将背后的分布式文件系统HDFS隐藏起来,杜绝以文件的视角去看待数据仓库。从Datastream导入日志数据之前用户需要指定日志的Serde、表定义,以及借助Storm或者Spark Streaming这样的流式处理工具对数据做预处理。联系到上一节提到的数据血缘分析,利用Hive工具可以利用SQL语句快速对表和表建立联系。数据开发之间可以共享UDF以及UDAF,猛犸需要对用户编辑的UDF和UDAF进行统一的管理,允许组内共通,同时猛犸也能够提供UDF/UDAF的开发框架,方便用户快速编写自定义函数。

数据平台的管理

这里说的数据平台管理不止是对于平台系统的管理,猛犸目前即成的管理功能包括有用户资源分配。用户目录权限审批,用户组管理等等。只有这些是远远不够的。

平台管理功能,更应该暴露出平台上的每个用户、每个项目的运行情况。具体包括:

用户资源占用量

用户资源包括存储、计算等等,存储资源比较好理解,即用户所有的文件占用了多少分布式文件系统的空间,这份数据有助于更好的来规划集群的存储资源,发现用量增长过快的用户,帮助其合理的规划资源。各种表、各种项目的top排名能够使运维人员一目了然当前集群的负载状况。 计算资源相对更难衡量,只能根据用户队列的使用情况来获取。

用户任务运行情况

用户任务运行情况由任务调度系统来获取,用户由多少Task, 每个Task包涵多少Job, Job是什么类型,每个Job/Task分别需要多少时间执行,哪些Job失败的概率最高,失败的原因是什么等等,这些统计都非常的重要。猛犸目前使用的Azkaban调度系统可以说是一套难以规划用量与资源的系统,目前所有的用户使用一套调度系统,这套调度系统本身缺乏调度隔离,可以说在资源不足的情况下用户之间很容易受到影响,作为平台管理合理规划任务调度资源就需要依赖以上的这些统计信息。其中Job失败原因分析是急需解决的问题,经验不足的数据开发人员往往定位错误原因的效率不高,而平台可以提供一套行之有效的规则体系,提取错误日志信息,推断错误原因。结合起来。假如出现用户的某些JOB非常高的频率因为内存不足而提交失败,则可以促使系统扩容或者是协助用户调整触发时间来达到修正。假如多次出现数据库权限导致的Sqoop任务失败,则可以联系DBA尽早排查出ACL或者白名单的缺失问题。

未来

一个大数据平台的完善是根据底层技术以及用户的需求更新不断地发展的,猛犸需要以提供完整的解决方案为思路,引入更多更好的功能和工具满足不同人群对于数据的需求。实时数据处理、更丰富数据同步、日志检索、KeyValue系统、算法模块都是未来打算引入的内容。用户对于数据的需求是越来越丰富的,在大数据时代已经没有一招鲜的解决方案了,只有见招拆招,有针对性的主打细分的场景,逐个击破才是近期的发展方向。猛犸的初衷不会改变,降低大数据的门槛,提供更全面、易用的大数据服务。

Categories: Uncategorized Tags:

MySQL 5.7新特性介绍

October 31st, 2016    阅读(186) No comments

1. 介绍

身处MySQL这个圈子,能够切身地感受到大家对MySQL 5.7的期待和热情,似乎每个人都迫不及待的想要了解、学习和使用MySQL 5.7。那么,我们不禁要问,MySQL 5.7到底做了哪些改进,引入了哪些新功能,性能又提升了多少,能够让大家翘首以盼,甚至欢呼雀跃呢?

此处输入图片的描述

下面就跟随我来一起了解一下MySQL 5.7的部分新功能。想要在一篇文章中介绍完MySQL 5.7的所有改进,几乎是不可能的。所以,我会选择一些有特别意思的、特别有用的功能进行介绍。希望通过这篇文章,能够激发大家对MySQL 5.7的学习兴趣,甚至能够吸引大家将自己的业务迁移到MySQL 5.7上。

MySQL 5.7在诸多方面都进行了大幅的改进,本文将从安全性(见2.1节)、灵活性(见2.2节)、易用性(见2.3节)、可用性(见2.4节)和性能(见2.5节)等几个方面进行介绍。最后,在第3节对本文进行了简单的总结。

2. MySQL 5.7的新特性

这一节中,将依次介绍MySQL 5.7的各种新特性。由于MySQL 5.7改进较多,因此,本文将这些新特性进行了简单的分类,分为安全性、灵活性、易用性、可用性和性能。接下来,将从各个分类依次进行介绍。

2.1 安全性

安全性是数据库永恒的话题,在MySQL 5.7中,有不少安全性相关的改进。包括:

  • MySQL数据库初始化完成以后,会产生一个root@localhost用户,从MySQL 5.7开始,root用户的密码不再是空,而是随机产生一个密码,这也导致了用户安装5.7时发现的与5.6版本比较大的一个不同点
  • MySQL官方已经删除了test数据库,默认安装完后是没有test数据库的,就算用户创建了test库,也可以对test库进行权限控制了
  • MySQL 5.7版本提供了更为简单SSL安全访问配置,并且默认连接就采用SSL的加密方式
  • 可以为用户设置密码过期策略,一定时间以后,强制用户修改密码
    ALTER USER 'jeffrey'@'localhost' PASSWORD EXPIRE INTERVAL 90 DAY;
    
  • 可以”锁”住用户,用以暂时禁用某个用户
    ALTER USER  'jeffrey'@'localhost' ACCOUNT LOCK;
    ALTER USER l 'jeffrey'@'localhost'  ACCOUNT UNLOCK;
    

2.2 灵活性

在这一节,我将介绍MySQL 5.7的两个全新的功能,即JSON和generate column。充分使用这两个功能,能够极大地提高数据存储的灵活性。

2.2.1 JSON

随着非结构化数据存储需求的持续增长,各种非结构化数据存储的数据库应运而生(如MongoDB)。从最新的数据库使用排行榜来看,MongoDB已经超过了PostgreSQL,其火热程度可见一斑。

各大关系型数据库也不甘示弱,纷纷提供对JSON的支持,以应对非结构化数据库的挑战。MySQL数据库从5.7.8版本开始,也提供了对JSON的支持。其使用方式如下:

CREATE TABLE t1 (jdoc JSON);
INSERT INTO t1 VALUES('{"key1": "value1", "key2": "value2"}');

MySQL对支持JSON的做法是,在server层提供了一堆便于操作JSON的函数,至于存储,就是简单地将JSON编码成BLOB,然后交由存储引擎层进行处理,也就是说,MySQL 5.7的JSON支持与存储引擎没有关系,MyISAM 存储引擎也支持JSON 格式。

MySQL支持JSON以后,总是避免不了拿来与MongoDB进行一些比较。但是,MySQL对JSON的支持,至少有两点能够完胜MongoDB:

  1. 可以混合存储结构化数据和非结构化数据,同时拥有关系型数据库和非关系型数据库的优点
  2. 能够提供完整的事务支持

2.2.2 generate column

generated column是MySQL 5.7引入的新特性,所谓generated column,就是数据库中这一列由其他列计算而得。

例如,知道直角三角形的两条直角边,要求直角三角形的面积。很明显,面积可以通过两条直角边计算而得,那么,这时候就可以在数据库中只存放直角边,面积使用generated column,如下所示:

CREATE TABLE triangle (sidea DOUBLE, sideb DOUBLE, area DOUBLE AS (sidea * sideb / 2));
insert into triangle(sidea, sideb) values(3, 4);
select * from triangle;
+-------+-------+------+
| sidea | sideb | area |
+-------+-------+------+
|     3 |     4 |    6 |
+-------+-------+------+

在MySQL 5.7中,支持两种generated column,即virtual generated column和stored generated column,前者只将generated column保存在数据字典中(表的元数据),并不会将这一列数据持久化到磁盘上;后者会将generated column持久化到磁盘上,而不是每次读取的时候计算所得。很明显,后者存放了可以通过已有数据计算而得的数据,需要更多的磁盘空间,与virtual column相比并没有优势。因此,在不指定generated column的类型时,默认是virtual column,如下所示:

show create table triangle\G
*************************** 1. row ***************************
       Table: triangle
Create Table: CREATE TABLE `triangle` (
  `sidea` double DEFAULT NULL,
  `sideb` double DEFAULT NULL,
  `area` double GENERATED ALWAYS AS (((`sidea` * `sideb`) / 2)) VIRTUAL
) ENGINE=InnoDB DEFAULT CHARSET=latin1

如果读者觉得generate column提供的功能,也可以在用户代码里面实现,并没有什么了不起的地方,那么,或许还有一个功能能够吸引挑剔的你,那就是为generate column创建索引。在这个例子中,如果我们需要根据面积创建索引以加快查询,就无法在用户代码里面实现,使用generate column就变得非常简单:

alter table triangle add index ix_area(area);

2.3 易用性

易用性是数据库永恒的话题,MySQL也在持续不断地提高数据库的易用性。在MySQL 5.7中,有很多易用性方面的改进,小到一个客户端快捷键ctrl+c的使用,大到专门提供一个系统库(sys)来帮助DBA和开发人员使用数据库。这一节将重点介绍MySQL 5.7引入的sys库。

  • 在linux下,我们经常使用ctrl+c来终止一个命令的运行,在MySQL 5.7 之前,如果用户输入了错误的SQL语句,按下ctrl+c,虽然能够”结束”SQL语句的运行,但是,也会退出当前会话,MySQL 5.7对这一违反直觉的地方进行了改进,不再退出会话。
  • MySQL 5.7可以explain一个正在运行的SQL,这对于DBA分析运行时间较长的语句将会非常有用
  • 在MySQL 5.7中,performance_schema提供了更多监控信息,包括内存使用,MDL锁,存储过程等

2.3.1 sys schema

sys schema是MySQL 5.7.7中引入的一个系统库,包含了一系列视图、函数和存储过程, 该项目专注于MySQL的易用性。例如,我们可以通过sys schema快速的知道,哪些语句使用了临时表,哪个用户请求了最多的io,哪个线程占用了最多的内存,哪些索引是无用索引等

sys schema中包含了大量的视图,那么,这些视图的信息来自哪里呢?视图中的信息均来自performance schema统计信息。这里有一个很好的比喻:

For Linux users I like to compare performance_schema to /proc, and SYS to vmstat.

也就是说,performance schema提供了信息源,但是,没有很好的将这些信息组织成有用的信息,从而没有很好的发挥它们的作用。而sys schema使用performance schema信息,通过视图的方式给出解决实际问题的答案。

例如,下面这些问题,在MySQL 5.7之前,需要借助外部工具才能知道,在MySQL 5.7中,直接查询sys库下相应的表就能得到答案:

  • 如何查看数据库中的冗余索引
    select * from sys.schema_redundant_indexes;
    
  • 如何获取未使用的索引
    select * from schema_unused_indexes;
    
  • 如何查看使用全表扫描的SQL语句
    select * from statements_with_full_table_scans
    

2.4 可用性

MySQL 5.7在可用性方面的改进也带给人不少惊喜。这里介绍特别有用的几项改进,包括:

  • 在线设置复制的过滤规则不再需要重启MySQL,只需要停止SQL thread,修改完成以后,启动SQL thread
  • 在线修改buffer pool的大小

    MySQL 5.7为了支持online buffer pool resize,引入chunk的概念,每个chunk默认是128M,当我们在线修改buffer pool的时候,以chunk为单位进行增长或收缩。这个参数的引入,对innodb_buffer_pool_size的配置有了一定的影响。innodb要求buffer pool size是innodb_buffer_pool_chunk_size* innodb_buffer_pool_instances的倍数,如果不是,将会适当调大innodb_buffer_pool_size,以满足要求,因此,可能会出现buffer pool的实际分配比配置文件中指定的size要大的情况

  • Online DDL
    MySQL 5.7支持重命名索引和修改varchar的大小,这两项操作在之前的版本中,都需要重建索引或表

    ALTER TABLE t1 ALGORITHM=INPLACE, CHANGE COLUMN c1 c1 VARCHAR(255);
    
  • 在线开启GTID,在之前的版本中,由于不支持在线开启GTID,用户如果希望将低版本的数据库升级到支持GTID的数据库版本,需要先关闭数据库,再以GTID模式启动,所以导致升级起来特别麻烦。MySQL 5.7以后,这个问题不复存在

2.5 性能

性能一直都是用户最关心的问题,在MySQL每次新版本中,都会有不少性能提升。在MySQL 5.7中,性能相关的改进非常多,这里仅介绍部分改进,包括临时表相关的性能改进、只读事务的性能优化、连接建立速度的优化和复制性能的改进。

2.5.1 临时表的性能改进

MySQL 5.7 为了提高临时表相关的性能,对临时表相关的部分进行了大幅修改,包括引入新的临时表空间;对于临时表的DDL,不持久化相关表定义;对于临时表的DML,不写redo,关闭change buffer等。所有临时表的改动,都基于以下两个事实

  1. 临时表只在当前会话中可见
  2. 临时表的生命周期是当前连接(MySQL宕机或重启,则当前连接结束)

也就是说,对于临时表的操作,不需要其他数据一样严格地进行一致性保证。通过不持久化元信息,避免写redo等方式,减少临时表操作的IO,以提高临时表操作的性能。

2.5.2 只读事务性能改进

众所周知,在传统的OLTP应用中,读操作远多于写操作,并且,读操作不会对数据库进行修改,如果是非锁定读,读操作也不需要进行加锁。因此,对只读事务进行优化,是一个不错的选择。

在MySQL 5.6中,已经对只读事务进行了许多优化。例如,将MySQL内部实现中的事务链表分为只读事务链表和普通事务链表,这样在创建ReadView的时候,需要遍历事务链表长度就会小很多。

在MySQL 5.7中,首先假设一个事务是一个只读事务,只有在该事务发起了修改操作时,才会将其转换为一个普通事务。MySQL 5.7通过避免为只读事务分配事务ID,不为只读事务分配回滚段,减少锁竞争等多种方式,优化了只读事务的开销,提高了数据库的整体性能。

2.5.3 加速连接处理

在MySQL 5.7之前,变量的初始化操作(THD、VIO)都是在连接接收线程里面完成的,现在将这些工作下发给工作线程,以减少连接接收线程的工作量,提高连接的处理速度。这个优化对那些频繁建立短连接的应用,将会非常有用。

2.5.4 复制性能的改进

MySQL的复制延迟是一直被诟病的问题之一,欣喜的是,MySQL 5.7版本已经支持”真正”的并行复制功能。MySQL 5.7并行复制的思想简单易懂,简而言之,就是”一个组提交的事务都是可以并行回放的”,因为这些事务都已进入到事务的prepare阶段,则说明事务之间没有任何冲突(否则就不可能提交)。MySQL 5.7以后,复制延迟问题永不存在。

这里需要注意的是,为了兼容MySQL 5.6基于库的并行复制,5.7引入了新的变量slave-parallel-type,该变量可以配置成DATABASE(默认)或LOGICAL_CLOCK。可以看到,MySQL的默认配置是库级别的并行复制,为了充分发挥MySQL 5.7的并行复制的功能,我们需要将slave-parallel-type配置成LOGICAL_CLOCK。

3. 总结

  1. 从本文中可以看到,MySQL 5.7确实带来了很多激动人心的功能,我们甚至不需要进行任何修改,只需要将业务迁移到MySQL 5.7上,就能带来不少性能的提升。
  2. 从本文中还可以看到,虽然MySQL 5.7在易用性上有了很多的改进,但是,也有不少需要注意的地方, 例如:1)在设置innodb的buffer pool时,需要注意chunk的存在,合理设置buffer pool instance否则可能出现实际分配的buffer pool size比预想的大很多的情况;2)多线程复制需要注意将slave_parallel_type设置为LOGICAL_CLOCK,否则,MySQL使用的是库级别的并行复制,对于大多数应用,并没有什么效果。那么,怎样才是使用MySQL 5.7的正确姿势呢?网易蜂巢是一个不错的选择,网易蜂巢的RDS(Relational Database Service,简称RDS)项目是一种即开即用、稳定可靠、可弹性伸缩的在线数据库服务。使用RDS提供的服务,就是使用已经调优过的数据库,用户不需要对数据库参数进行任何修改,就能够获得一个性能极好的数据库服务。

4. 参考资料

  1. What’s New in MySQL 5.7
  2. What Is New in MySQL 5.7
  3. MySQL 5.7 并行复制实现原理与调优
Categories: Uncategorized Tags:

NOS的利器—富媒体处理

October 26th, 2016    阅读(161) No comments

NOS富媒体处理介绍

1.前言

NOS全称是:NetEase Object Storage,是一个Key-Value存储系统。NOS从2012年下半年开始给各个产品提供服务,到现在已经经历了将近4年的时间,经过4年的努力,NOS在网易已经成为了一个不可或缺的模块。到目前为止NOS取得的成就如下:

  • 产品数量:100+
  • 桶的数量:700+
  • 对象数量:9400000000+
  • 物理存储:6800T
  • 常规日增:20T
  • QPS峰值:16000+

NOS目前主要服务包括:核心存储,上传加速,富媒体处理三大块内容。本文将对用户询问最多、最感兴趣的富媒体处理这个模块从非技术的角度进行详细的介绍。

2.富媒体处理简介

富媒体处理是NOS推出的对图片、视频、音频进行实时处理的一个模块,支持用户对自己的富媒体文件进行定制化处理。该模块一经推出便受到了广大用户的热烈欢迎(此处应有掌声!!!)。到目前为止有大量的用户在使用该项服务,比如:lofter、漫画、云音乐、云阅读等等。富媒体处理之所以这么受欢迎,是因为它具有以下特征:

  • 接口简单易用

具体表现在屏蔽了富媒体处理的复杂逻辑,用户只需要通过url的一些参数设置就可以得到想要的富媒体文件。

  • 处理高效

富媒体处理在内存、性能方面做了很多优化,处理速度达到了非常高的水平。并且采用了分布式架构,可以很容易进行水平扩充。

  • 可靠性高

富媒体处理将处理复杂耗时的一些请求和简单的请求隔离开来,少量的这种耗时的处理对富媒体整体的处理影响很小。增加了富媒体处理的可靠性和防攻击性。

  • 成本低

由于NOS的存在,不同的业务之间不需要各自再实现富媒体处理的逻辑,降低了公司的成本。

到现在为止富媒体处理的一些相关数据为:

  • 平均处理时间:200ms
  • 缓存命中比例:70+
  • QPS峰值:10000+

富媒体处理主要包括图片的常见处理,视频截图,音频转码等几个部分,接下来的章节会对每一部分做详细的介绍。

3.图片处理

图片处理包括非常丰富的接口,具体的内容如下图:

Alt pic

3.1 图片元信息

EXIF是“可交换图像文件”的缩写,EXIF文件中会记录图片的拍摄参数、图片的宽高、作者等元数据信息。NOS提供了exif关键字供用户获取图片的元信息。比如:

GET http://nos.netease.com/img-sample/test/1.jpg?exif

exif.png

NOS的接口是非常友好的,不光能够返回json格式的内容,还可以通过HTTP头指定xml格式的返回内容。

3.2 基本图片处理

3.2.1 图片操作

NOS提供的基本图片处理功能异常丰富,基本涵盖了绝大部分的用户需求,主要包括:

  • 图片缩放

缩放功能是用户使用最多的一项功能。用户上传的图片各种尺寸的都有,如果没有图片缩放功能,在前端页面无法很好的展示,不同尺寸的图片会完全打乱前端的显示框架,因此前端迫切的希望收到的图片是固定大小的。而现在只需要告诉NOS希望的图片长宽即可实现这个神奇的功能。NOS不光可以按照长宽做缩放,还可以按照像素做图片缩放。

  • 图片裁剪

顾名思义,裁剪就是按照用户给定的位置和大小对图片进行裁剪。

  • 类型转换

类型转换在NOS使用的也是非常多的功能。目前支持常见的几种图片类型:gif,jpg,jpeg,jng,png,webp。

  • 图片填充

此处的填充指的是背景填充。给定一个图片大小,当原图的实际尺寸小于这个大小时,会使用指定的颜色对图片进行填充。NOS最近开发了自适应图片填充这个新功能。

自适应图片填充: 当用户只给定图片长宽的其中一个值时,NOS会只按照一个长或宽的值进行填充,另一个值采用原图的大小进行自适应。

  • 图片模糊

图片模糊采用的是高斯噪声来对图片进行模糊处理,通常用它来减少图像噪声以及降低细节层次,效果比较明显。

  • 图片旋转

图片旋转是通过修改图片的rotate来对图片进行旋转的。

  • 去除元信息

元信息是嵌入到图片中的注释信息,会增大图片的质量,在实际中为了减少图片质量,往往会将图片的原信息去掉。

  • 图片interlace

interlace是指图片渐进式加载,在网络情况比较差的环境下,使用interlace参数后会根据收到图片的内容渐进式的显示图片。

3.2.2 图片类型——WEBP

NOS目前支持主流的几种图片类型,包括gif,jpg,jpeg,jng,png等。不管是 PC 还是移动端,图片一直是流量大头,以苹果公司 Retina 产品为代表的高 PPI 屏对图片的质量提出了更高的要求,如何保证在图片的精细度不降低的前提下缩小图片体积,成为了一个有价值且值得探索的事情。而如今对于 JPEG、PNG 和 GIF 这些图片格式的优化几乎已经达到了极致。

2010年10月份 Google 宣布了一种新的图片格式WebP,它可以将图片大小减少 40%,目的是替代当前的图片标准 JPEG,2011年11月Google宣布了 WebP 图片格式的一些改进,加入了透明格式支持,所以它同样也想取代掉 PNG 格式。经过测试webp的压缩效果非常好:

! webp.png

和jpeg相比,有损的webp压缩比例更高,图片清晰度也更高,优势是十分明显的。唯一的不足在于webp的浏览器兼容性还有待提高。

3.3 GIF处理

GIF的处理分为两大类,基本图片处理和GIF合成。

3.3.1 基本图片处理

GIF的基本图片处理和3.1介绍的大体相同,稍许的差别是GIF支持生成动图和静图。

3.3.2 GIF合成

通过指定需要合成GIF的图片的列表,NOS会自动将这些图片合成GIF格式。合成GIF的时候NOS提供的服务很自由,用户可以自己指定GIF的一些相关参数,从而是GIF更符合自己的需求。具体的参数参数有:

参数 节点描述
宽度 用户可以不关心需要合成GIF图片的大小,只需要给出合成后GIF图片的大小即可
高度 同上
播放速度 用户可以自己指定帧与帧之间的播放时间间隔
循环间隔 GIF在循环播放的时候,用户可以控制循环的时间间隔

3.4 水印

NOS水印支持图片水印和文字水印两种形式。

3.4.1 图片水印

图片水印是将图片作为水印嵌入到一个新的图片中。图片水印支持多种图片格式:jpg,jng,png,webp,bmp,jpeg。NOS将图片按照九宫格的形式划分为九个区间,先选择区间再在每个区间按照像素的位移选择最终的位置放置logo。

Alt pic

图片水印具有以下特色功能:

  • 支持调节水印透明度,从不透明到完全透明的变化
  • 元数据的去除:元数据往往会占用图片不小的空间,去除元数据后的水印会明显降低图片大小。

3.4.2 文字水印

针对文字水印用户可以自己指定文字的内容、字体大小、字体颜色等参数,至于水印的位置和图片水印设置一样。比如下面这张水印图片:

Alt pic

NOS支持非常丰富的字体类型,常用的有:

参数 文件 字体
simfang simfang.ttf 仿宋
simhei simhei.ttf 黑体
simkai simkai.ttf 楷体
simsun simsun.ttc 宋体
msyh msyh.ttf 微软雅黑
msyhbd msyhbd.ttf 微软雅黑(粗体)

还有其它不太常用的字体,在此就不一一赘述。

4.视频音频处理

NOS对视频的处理相对简单,目前只支持视频信息获取、视频截图两项功能。音频也只支持转码功能

4.1 视频信息获取

和图片基本信息获取有些类似,提供通过关键字获取视频基本信息的功能。主要的视频信息如下:

  • 帧速
  • 视频宽高
  • 视频大小
  • 编码类型
  • 视频比特率
  • 音频比特率

4.2 视频截图

NOS提供了针对视频的截图功能,具体截哪一个帧是通过时间来控制的,用户可以指定时间点(目前只精确到秒)来截取该时间点对应的帧。根据调研,其实大多数用户只关心视频的第一帧,最多的需求也是获取第一帧。所以精确到秒基本满足了用户的需求。

出于方面用户的考虑,NOS还提供了截图的后续处理,在截图完成后可以直接进行部分图片操作。目前支持的图片操作有:

  • 图片裁剪
  • 图片缩放
  • 类型转换

4.3 音频转码

音频转码目前只提供对常见音频进行转码的处理。支持的音频格式有:amr、aac、MP3。在转码的过程中用户可以对音频以下三个参数进行设置:

  • 音量
  • 采样率
  • 比特率

视频音频NOS的服务并不丰富,如果大家有这方面更多的需求,可以联系视频云团队寻求支持。

5. 后记

富媒体处理经过几年的开发维护,目前已经十分的稳定,而且成长到现在这个水平是值得NOS团队每个人为之骄傲的。但我们不会停止不前,富媒体处理仍然有许多内容待开发,比如:域外图片拉取,图片压缩优化等等。NOS团队会继续努力为用户提供更加优质的服务。

Categories: Uncategorized Tags:

给Ceph插上一双翅膀

October 17th, 2016    阅读(320) No comments

Ceph与SPDK、DPDK

引子

Ceph是一个优秀的开源分布式存储系统,一个系统同时做到对象存储、文件存储、块设备存储,号称统一存储。伴随着云计算社区Openstack一起快速发展。NBS从年初开始在线上部署使用了Ceph,磁盘全部使用SSD,来提供块存储服务。在Ceph的使用过程中,我们已经充分感受到Ceph在高可用、高可靠、易部署、易维护方面的优点,Ceph本身的设计也算得上优秀,NBS小组之前也针对Ceph的设计做了一系列的分享。但是作为一个存储系统除了高可用、高可靠外,我们还希望能获得高性能,而Ceph在这方面就有点差强人意,至少在其块存储服务性能上,还有很大的提升空间。我们在Ceph的使用和测试过程中发现Ceph本身对CPU资源消耗比较严重,计算资源竟然成为了系统的瓶颈,以致无法发挥出SSD磁盘应有的性能。我们在测试和分析中发现,其原因一方面是Ceph实现中在网络连接、多线程、锁粒度等方面还需要做进一步的优化,另一方面是伴随着现在网络、存储设备性能的提升,在高IO负载的情况下,系统软件、存储软件本身在IO消耗中占的比例越来越高。 抛开Ceph我们来看一些性能方面的数据对比:

在如今已经普遍使用的万兆网卡下,每秒最多可以收发14.88millions(Packets Per Second)的数据包,那么一个数据包的处理时间,只需要大概67ns,在2.0GHz的CPU下,只要130个时钟周期。而做一次上下文切换则大概需要上千ns(How long does it take to make a context switch?)。现在PCI-Express x16的速度已经达到5GBytes/s,而CPU访问内存的带宽也就6-8GBytes/s(Memory_bandwidth)。可见硬件的性能有了很大的提高,而软件成了制约性能的瓶颈,这就好比我的出行,从出发地到目的地,交通工具的速度从普通火车到高铁、到飞机了,但是候车(机)、安检却还是花费很多时间,我们的旅行时间还是无法降低下来。

DPDK

概述

DPDK(Data Plane Development Kit)就是以Intel为首的厂商为解决上述网络问题而提出的解决方案,在引入到存储领域之前,在软件定义网络(SDN)方面已经有了很多重要的应用,它实际上是为解决x86体系下网络传输效率的一系列技术手段的集合。

多个技术

其中最重要的一项就是采用轮询方式(poll-mode drivers)来实现数据包的处理。

PMDs

操作系统处理IO可以有两种方式:一种是利用中断(interrupt),即I/O设备有IO事件时,产生一个CPU中断,来通知CPU;另外也可以通过轮询,即CPU不断的检测I/O设备,查看是否有IO事件。当I/O设备较慢或者说IO较少时,无疑中断是一种高效的方式,没有IO时,CPU可以进行其他的工作,而轮询的方式,CPU需要浪费很多时钟周期用来检测,IO响应可能还没有中断更及时。但是当I/O设备很快、IO很多时,情况就不一样了,我们知道中断也是有代价的,CPU响应中断时,需要保存现场(content),处理完中断后需要返回并恢复现场,也就是我们通常说的context switch。试想当IO密集、中断很多的情况下,CPU就把很多时钟周期(clock cycles)浪费在了context switch上。随着现在I/O设备性能越来越高,这个问题在现实中也是变的越来越严重。这时反而利用轮询的方式,反而可以提高数据处理的效率。实际上DPDK和SPDK正是在这样的背景下产生的。

在X86结构中,处理数据包的处理方式就是通过CPU中断方式,既网卡驱动接收到数据包后通过中断通知CPU处理,然后由CPU拷贝数据并交给协议栈(TCP/IP Stack),协议栈在内核态完成协议处理后,再交给用户态的应用程序进行数据的处理。在数据量大时,这种方式会产生大量CPU中断,导致CPU无法运行其他程序。而DPDK则采用轮询方式(PMD)实现数据包处理过程:DPDK重载了网卡驱动,该驱动在收到数据包后不中断通知CPU,而是将数据包通过零拷贝技术存入内存,这时应用层程序就可以通过DPDK提供的接口,直接从内存读取数据包。这种处理方式节省了CPU中断时间、内存拷贝时间,并向应用层提供了简单易行且高效的数据包处理方式,使得网络应用的开发更加方便。

通过kernel驱动的包处理流程:

Alt pic

DPDK模式下包处理流程:

Alt pic

UIO

上面DPDK的数据包处理整个过程都是工作在用户态,不需要CPU介入,不仅节省了CPU中断,还节省了进程内核态和用户态的切换,所以会“节省”很多CPU资源。这样从理论上说起来其实也是好理解的,但是可能还是会觉得有违我们传统上的认识。《Linux Devic Driver》中有这样一句话,相比看过的都会影响深刻,“The most important devices can’t be handled in user space, including, but not limited to, network interfaces and block devices.” 。实际上就像我们前面说到的,这些都是在I/O设备性能迅猛提高的背景下,以及分布式系统的广泛应用、我们期望能在普通的x86体系下,达到媲美甚至超过传统存储系统的性能,而产生的技术变化。UIO模式下的IO栈变化:

kernel驱动的IO栈:

Alt pic

UIO:

Alt pic

除了采用PMD和UIO,DPDK还采用了下面一些技术。

hugepage

我们知道linux在进行内存管理时,分为物理内存和虚拟内存,由内存管理单元(MMU)完成虚拟内存到物理内存的地址映射,虚拟内存按照页(pages)来管理,大小为4k,MMU进行地址转换需要的信息保存在称为页表(page table)的数据结构中,这样系统在访问一次内存时,需要访问两次内存:首先访问pagetable,获得物理地址,然后再次访问获取数据,实际上pagetable在实现上还往往分为多级(比如page directory + page table)。为了优化这个过程,我们知道CPU会有一个称为TLB的缓存器,来缓存虚拟地址和物理地址的映射信息,减少一次多余的内存访问。但是既然是cache,就肯定会有缓存不命中(miss)的情况,miss时就多出至少一次内存访问。无疑pagetable信息越多或者说page越多的情况下,就会需要越多的TLB空间,空间一定的情况下,miss的概率就会响应变大。TLB类似L1、L2,访问是非常快的,一般只需要0.5到1个时钟周期(clock cycles),而一次内存操作则可能需要几十个操场,可以看出相对来说页表查找是一种很耗时的操作。为了优化上述问题,引入了hugepage,简单来说就是增大page到小,这样可以减少pagetable信息,一个 TLB 表项可以指向更大的内存区域,这样可以 大幅减少 TLB miss 的发生。DPDK 则利用大页技术,所有的内存都是从 HugePage 里分配,实现对内存池(mempool)的管理,并预先分配好同样大小的mbuf,供每一个数据包使用。

CPU affinity

现代操作系统都是基于分时调用方式来实现任务调度,多个进程或线程在多核处理器的某一个核上不断地交替执行。每次切换过程,都需要将处理器的状态寄存器保存在堆栈中, 并恢复当前进程的状态信息,这对系统其实是一种处理开销。将一个线程固定一个核上运行,可以消除切换带来的额外开销。另外将进程或者线程迁移到多核处理器的其它核上进行运行时,处理器缓存中的数据也需要进行清除,导致处理器缓存的利用效果降低。 CPU 亲和技术,就是将某个进程或者线程绑定到特定的一个或者多个核上执行,而不被 迁移到其它核上运行,这样就保证了专用程序的性能。DPDK 使用了 Linux pthread 库,在系统中把相应的线程和CPU进行亲和性绑定,然后相应的线程尽可能使用独立的资源进行相关的数据处理。

除了上述技术,DPDK中还包括很多方面的优化,比如优秀的内存、缓存管理,环形无锁队列等等,使得通过DPDK可以达到非常高效的网络包处理。值的说一下的是,DPDK需要网卡的硬件支持,不过DPDK是一个开源的项目,不仅仅是intel的网卡才支持。

SPDK

概述

SPDK(Storage Performance Development Kit )也是由intel主导的一个项目,目前也已经开源。SPDK是伴随着存储设备的性能提升而发展出来的技术。如今闪存设备已经开始广泛应用,我们在部署使用Ceph时已经是用的全SSD(SATA SSD)。SATA SSD相比于机械硬盘,性能已经有几十倍的提升,后续再采用PCIe/NVMe SSD性能将再有几十倍的提升。以前磁盘IO是最慢的一环,所以其他环境的性能消耗相对都是少数,如今磁盘性能提升很大,相比于存储设备的提升,我们的存储软件架构则没有大的变化,软件的消耗在IO整体时间上占的比重越来越大。SPDK就是来解决这个问题的一个方案,也是多项技术的集合,和DPDK的思路一样,最重要的两点就是PMD和UIO。PMDs对于存储设备来说就是改变之前存储设备的驱动模式,UIO将磁盘IO用户态话避免内核上下文切换和中断处理,减少CPU消耗,这些跟上面DPDK的思路是一样的,这里就不展开了。

CEPH与DPDK、SPDK

目前在CEPH系统CPU瓶颈没办法得到很好的解决的情况下,社区开始引入DPDK和SPDK,国内的一家创业存储企业xsky已经向社区提交了Ceph对DPDK的支持方案,方式是重写了async messenger,然后DPDK(DPDKStack)和kernal tcp/ip(PosixStack)一样作为async messenger的插件。加上后续对SPDK的支持,将形成IO路径全用户态、全异步的模式,将性能发挥到极致。真的像是跟CEPH插上两根翅膀一样,如虎添翼,在统一存储,软件自定义存储(SDS)领域将得到更好的发展。

与DPDK、SPDK结合后Ceph的IO路径:

Alt pic

 

参考资料

http://dpdk.org/

http://www.slideshare.net/garyachy/dpdk-44585840

https://software.intel.com/en-us/articles/introduction-to-the-storage-performance-development-kit-spdk

Categories: Uncategorized Tags: