Flink 官方文档解读-导读

该文章,写给想看开源项目,不知道怎么看,或者不会看的朋友

访问Flink官方地址:https://flink.apache.org/ ,可以通过Apache官方 www.apache.org 点击Project或者直接搜索“Flink”

我的一个阅读习惯,从Documentation开始,然后找到最新的一个Release版本,当前是1.7

访问页面,我们会看到上面的页面,其中,一些知识点,都被加上了超链接,后续我们单独介绍,先来看看这一页说了什么内容。

一上来,先说了一些文档的编辑时间。说了一下Flink是一个分布式流处理和批处理的开源平台,Flink核心是一个流式处理引擎,他提供了数据分发,通信,在分布式计算数据流上进行数据容错,在留引擎上构建了批处理,包含本地迭代支持,内存管理,编程优化。

第一步,介绍一些概念,数据流编程模型分布式运行环境,它会帮助你理解其他章节,包括安装和编程,所以强烈建议你读一下。

初识

编程指南,你需要阅读我们的手册,包括基本API概念和去学习一下,DataStreaming APIDataSet API,如何去写一个流式程序。

部署

在你准备发布一个产品时,请先阅读产品清单

其他资源

一些讲座可以关注flink官方网站,或者看 YouTube

培训教材

博客data Artisans官方博客

美团外卖iOS App冷启动治理(转载)

一、背景

冷启动时长是App性能的重要指标,作为用户体验的第一道“门”,直接决定着用户对App的第一印象。美团外卖iOS客户端从2013年11月开始,历经几十个版本的迭代开发,产品形态不断完善,业务功能日趋复杂;同时外卖App也已经由原来的独立业务App演进成为一个平台App,陆续接入了闪购、跑腿等其他新业务。因此,更多更复杂的工作需要在App冷启动的时候被完成,这给App的冷启动性能带来了挑战。对此,我们团队基于业务形态的变化和外卖App的特点,对冷启动进行了持续且有针对性的优化工作,目的就是为了呈现更加流畅的用户体验。

二、冷启动定义

一般而言,大家把iOS冷启动的过程定义为:从用户点击App图标开始到appDelegate didFinishLaunching方法执行完成为止。这个过程主要分为两个阶段:

  • T1:main()函数之前,即操作系统加载App可执行文件到内存,然后执行一系列的加载&链接等工作,最后执行至App的main()函数。
  • T2:main()函数之后,即从main()开始,到appDelegate的didFinishLaunchingWithOptions方法执行完毕。

然而,当didFinishLaunchingWithOptions执行完成时,用户还没有看到App的主界面,也不能开始使用App。例如在外卖App中,App还需要做一些初始化工作,然后经历定位、首页请求、首页渲染等过程后,用户才能真正看到数据内容并开始使用,我们认为这个时候冷启动才算完成。我们把这个过程定义为T3。

综上,外卖App把冷启动过程定义为:从用户点击App图标开始到用户能看到App主界面内容为止这个过程,即T1+T2+T3。在App冷启动过程当中,这三个阶段中的每个阶段都存在很多可以被优化的点。

三、问题现状

性能存量问题

美团外卖iOS客户端经过几十个版本的迭代开发后,在冷启动过程中已经积累了若干性能问题,解决这些性能瓶颈是冷启动优化工作的首要目标,这些问题主要包括:

注:启动项的定义,在App启动过程中需要被完成的某项工作,我们称之为一个启动项。例如某个SDK的初始化、某个功能的预加载等。

性能增量问题

一般情况下,在App早期阶段,冷启动不会有明显的性能问题。冷启动性能问题也不是在某个版本突然出现的,而是随着版本迭代,App功能越来越复杂,启动任务越来越多,冷启动时间也一点点延长。最后当我们注意到,并想要优化它的时候,这个问题已经变得很棘手了。外卖App的性能问题增量主要来自启动项的增加,随着版本迭代,启动项任务简单粗暴地堆积在启动流程中。如果每个版本冷启动时间增加0.1s,那么几个版本下来,冷启动时长就会明显增加很多。

四、治理思路

冷启动性能问题的治理目标主要有三个:

  1. 解决存量问题:优化当前性能瓶颈点,优化启动流程,缩短冷启动时间。
  2. 管控增量问题:冷启动流程规范化,通过代码范式和文档指导后续冷启动过程代码的维护,控制时间增量。
  3. 完善监控:完善冷启动性能指标监控,收集更详细的数据,及时发现性能问题。

五、规范启动流程

截止至2017年底,美团外卖用户数已达2.5亿,而美团外卖App也已完成了从支撑单一业务的App到支持多业务的平台型App的演进(美团外卖iOS多端复用的推动、支撑与思考),公司的一些新兴业务也陆续集成到外卖App当中。下面是外卖App的架构图,外卖的架构主要分为三层,底层是基础组件层,中层是外卖平台层,平台层向下管理基础组件,向上为业务组件提供统一的适配接口,上层是基础组件层,包括外卖业务拆分的子业务组件(外卖App和美团App中的外卖频道可以复用子业务组件)和接入的其他非外卖业务。

App的平台化为业务方提供了高效、标准的统一平台,但与此同时,平台化和业务的快速迭代也给冷启动带来了问题:

  1. 现有的启动项堆积严重,拖慢启动速度。
  2. 新的启动项缺乏添加范式,杂乱无章,修改风险大,难以阅读和维护。

面对这个问题,我们首先梳理了目前启动流程中所有的启动项,然后针对App平台化设计了新的启动项管理方式:分阶段启动和启动项自注册

分阶段启动

早期由于业务比较简单,所有启动项都是不加以区分,简单地堆积到didFinishLaunchingWithOptions方法中,但随着业务的增加,越来越多的启动项代码堆积在一起,性能较差,代码臃肿而混乱。

通过对SDK的梳理和分析,我们发现启动项也需要根据所完成的任务被分类,有些启动项是需要刚启动就执行的操作,如Crash监控、统计上报等,否则会导致信息收集的缺失;有些启动项需要在较早的时间节点完成,例如一些提供用户信息的SDK、定位功能的初始化、网络初始化等;有些启动项则可以被延迟执行,如一些自定义配置,一些业务服务的调用、支付SDK、地图SDK等。我们所做的分阶段启动,首先就是把启动流程合理地划分为若干个启动阶段,然后依据每个启动项所做的事情的优先级把它们分配到相应的启动阶段,优先级高的放在靠前的阶段,优先级低的放在靠后的阶段。

下面是我们对美团外卖App启动阶段进行的重新定义,对所有启动项进行的梳理和重新分类,把它们对应到合理的启动阶段。这样做一方面可以推迟执行那些不必过早执行的启动项,缩短启动时间;另一方面,把启动项进行归类,方便后续的阅读和维护。然后把这些规则落地为启动项的维护文档,指导后续启动项的新增和维护。

通过上面的工作,我们梳理出了十几个可以推迟执行的启动项,占所有启动项的30%左右,有效地优化了启动项所占的这部分冷启动时间。

启动项自注册

确定了启动项分阶段启动的方案后,我们面对的问题就是如何执行这些启动项。比较容易想到的方案是:在启动时创建一个启动管理器,然后读取所有启动项,然后当时间节点到来时由启动器触发启动项执行。这种方式存在两个问题:

  1. 所有启动项都要预先写到一个文件中(在.m文件import,或用.plist文件组织),这种中心化的写法会导致臃肿的代码,难以阅读维护。
  2. 启动项代码无法复用:启动项无法收敛到子业务库内部,在外卖App和美团App中要重复实现,和外卖App平台化的方向不符。

而我们希望的方式是,启动项维护方式可插拔,启动项之间、业务模块之间不耦合,且一次实现可在两端复用。下图是我们采用的启动项管理方式,我们称之为启动项的自注册:一个启动项定义在子业务模块内部,被封装成一个方法,并且自声明启动阶段(例如一个启动项A,在独立App中可以声明为在willFinishLaunch阶段被执行,在美团App中则声明在resignActive阶段被执行)。这种方式下,启动项即实现了两端复用,不相关的启动项互相隔离,添加/删除启动项都更加方便。

那么如何给一个启动项声明启动阶段?又如何在正确的时机触发启动项的执行呢?在代码上,一个启动项最终都会对应到一个函数的执行,所以在运行时只要能获取到函数的指针,就可以触发启动项。美团平台开发的组件启动治理基建Kylin正是这样做的:Kylin的核心思想就是在编译时把数据(如函数指针)写入到可执行文件的__DATA段中,运行时再从__DATA段取出数据进行相应的操作(调用函数)。

为什么要用借用__DATA段呢?原因就是为了能够覆盖所有的启动阶段,例如main()之前的阶段。

Kylin实现原理简述:Clang 提供了很多的编译器函数,它们可以完成不同的功能。其中一种就是 section() 函数,section()函数提供了二进制段的读写能力,它可以将一些编译期就可以确定的常量写入数据段。 在具体的实现中,主要分为编译期和运行时两个部分。在编译期,编译器会将标记了 attribute((section())) 的数据写到指定的数据段中,例如写一个{key(key代表不同的启动阶段), *pointer}对到数据段。到运行时,在合适的时间节点,在根据key读取出函数指针,完成函数的调用。

上述方式,可以封装成一个宏,来达到代码的简化,以调用宏 KLN_STRINGS_EXPORT(“Key”, “Value”)为例,最终会被展开为:

__attribute__((used, section("__DATA" "," "__kylin__"))) static const KLN_DATA __kylin__0 = (KLN_DATA){(KLN_DATA_HEADER){"Key", KLN_STRING, KLN_IS_ARRAY}, "Value"};

使用示例,编译器把启动项函数注册到启动阶段A:

KLN_FUNCTIONS_EXPORT(STAGE_KEY_A)() { // 在a.m文件中,通过注册宏,把启动项A声明为在STAGE_KEY_A阶段执行
    // 启动项代码A
}
KLN_FUNCTIONS_EXPORT(STAGE_KEY_A)() { // 在b.m文件中,把启动项B声明为在STAGE_KEY_A阶段执行
    // 启动项代码B
}

在启动流程中,在启动阶段STAGE_KEY_A触发所有注册到STAGE_KEY_A时间节点的启动项,通过对这种方式,几乎没有任何额外的辅助代码,我们用一种很简洁的方式完成了启动项的自注册。

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    // 其他逻辑
    [[KLNKylin sharedInstance] executeArrayForKey:STAGE_KEY_A];  // 在此触发所有注册到STAGE_KEY_A时间节点的启动项
    // 其他逻辑
    return YES;
}

完成对现有的启动项的梳理和优化后,我们也输出了后续启动项的添加&维护规范,规范后续启动项的分类原则,优先级和启动阶段。目的是管控性能问题增量,保证优化成果。

六、优化main()之前

在调用main()函数之前,基本所有的工作都是由操作系统完成的,开发者能够插手的地方不多,所以如果想要优化这段时间,就必须先了解一下,操作系统在main()之前做了什么。main()之前操作系统所做的工作就是把可执行文件(Mach-O格式)加载到内存空间,然后加载动态链接库dyld,再执行一系列动态链接操作和初始化操作的过程(加载、绑定、及初始化方法)。这方面的资料网上比较多,但重复性较高,此处附上一篇WWDC的Topic:Optimizing App Startup Time 。

加载过程—从exec()到main()

真正的加载过程从exec()函数开始,exec()是一个系统调用。操作系统首先为进程分配一段内存空间,然后执行如下操作:

  1. 把App对应的可执行文件加载到内存。
  2. 把Dyld加载到内存。
  3. Dyld进行动态链接。

下面我们简要分析一下Dyld在各阶段所做的事情:

阶段工作
加载动态库Dyld从主执行文件的header获取到需要加载的所依赖动态库列表,然后它需要找到每个 dylib,而应用所依赖的 dylib 文件可能会再依赖其他 dylib,所以所需要加载的是动态库列表一个递归依赖的集合
Rebase和Bind– Rebase在Image内部调整指针的指向。在过去,会把动态库加载到指定地址,所有指针和数据对于代码都是对的,而现在地址空间布局是随机化,所以需要在原来的地址根据随机的偏移量做一下修正
– Bind是把指针正确地指向Image外部的内容。这些指向外部的指针被符号(symbol)名称绑定,dyld需要去符号表里查找,找到symbol对应的实现
Objc setup– 注册Objc类 (class registration)
– 把category的定义插入方法列表 (category registration)
– 保证每一个selector唯一 (selector uniquing)
Initializers– Objc的+load()函数
– C++的构造函数属性函数
– 非基本类型的C++静态全局变量的创建(通常是类或结构体)

最后 dyld 会调用 main() 函数,main() 会调用 UIApplicationMain(),before main()的过程也就此完成。

了解完main()之前的加载过程后,我们可以分析出一些影响T1时间的因素:

  1. 动态库加载越多,启动越慢。
  2. ObjC类,方法越多,启动越慢。
  3. ObjC的+load越多,启动越慢。
  4. C的constructor函数越多,启动越慢。
  5. C++静态对象越多,启动越慢。

针对以上几点,我们做了如下一些优化工作:

代码瘦身

随着业务的迭代,不断有新的代码加入,同时也会废弃掉无用的代码和资源文件,但是工程中经常有无用的代码和文件被遗弃在角落里,没有及时被清理掉。这些无用的部分一方面增大了App的包体积,另一方便也拖慢了App的冷启动速度,所以及时清理掉这些无用的代码和资源十分有必要。

通过对Mach-O文件的了解,可以知道__TEXT:__objcmethname:中包含了代码中的所有方法,而\_DATA__objc_selrefs中则包含了所有被使用的方法的引用,通过取两个集合的差集就可以得到所有未被使用的代码。核心方法如下,具体可以参考:objc_cover:

def referenced_selectors(path):
    re_sel = re.compile("__TEXT:__objc_methname:(.+)") //获取所有方法
    refs = set()
    lines = os.popen("/usr/bin/otool -v -s __DATA __objc_selrefs %s" % path).readlines() # ios & mac //真正被使用的方法
    for line in lines:
        results = re_sel.findall(line)
        if results:
            refs.add(results[0])
    return refs
}

通过这种方法,我们排查了十几个无用类和250+无用的方法。

+load优化

目前iOS App中或多或少的都会写一些+load方法,用于在App启动执行一些操作,+load方法在Initializers阶段被执行,但过多+load方法则会拖慢启动速度,对于大中型的App更是如此。通过对App中+load的方法分析,发现很多代码虽然需要在App启动时较早的时机进行初始化,但并不需要在+load这样非常靠前的位置,完全是可以延迟到App冷启动后的某个时间节点,例如一些路由操作。其实+load也可以被当做一种启动项来处理,所以在替换+load方法的具体实现上,我们仍然采用了上面的Kylin方式。

使用示例:

// 用WMAPP_BUSINESS_INIT_AFTER_HOMELOADING声明替换+load声明即可,不需其他改动
WMAPP_BUSINESS_INIT_AFTER_HOMELOADING() { 
    // 原+load方法中的代码
}
// 在某个合适的时机触发注册到该阶段的所有方法,如冷启动结束后
[[KLNKylin sharedInstance] executeArrayForKey:@kWMAPP_BUSINESS_INITIALIZATION_AFTER_HOMELOADING_KEY] 
}

七、优化耗时操作

在main()之后主要工作是各种启动项的执行(上面已经叙述),主界面的构建,例如TabBarVC,HomeVC等等。资源的加载,如图片I/O、图片解码、archive文档等。这些操作中可能会隐含着一些耗时操作,靠单纯阅读非常难以发现,如何发现这些耗时点呢?找到合适的工具就会事半功倍。

Time Profiler

Time Profiler是Xcode自带的时间性能分析工具,它按照固定的时间间隔来跟踪每一个线程的堆栈信息,通过统计比较时间间隔之间的堆栈状态,来推算某个方法执行了多久,并获得一个近似值。Time Profiler的使用方法网上有很多使用教程,这里我们也不过多介绍,附上一篇使用文档:Instruments Tutorial with Swift: Getting Started

火焰图

除了Time Profiler,火焰图也是一个分析CPU耗时的利器,相比于Time Profiler,火焰图更加清晰。火焰图分析的产物是一张调用栈耗时图片,之所以称为火焰图,是因为整个图形看起来就像一团跳动的火焰,火焰尖部是调用栈的栈顶,底部是栈底,纵向表示调用栈的深度,横向表示消耗的时间。一个格子的宽度越大,越说明其可能是瓶颈。分析火焰图主要就是看那些比较宽大的火苗,特别留意那些类似“平顶山”的火苗。下面是美团平台开发的性能分析工具-Caesium的分析效果图:

通过对火焰图的分析,我们发现了冷启动过程中存在着不少问题,并成功优化了0.3S+的时间。优化内容总结如下:

优化点举例
发现隐晦的耗时操作发现在冷启动过程中archive了一张图片,非常耗时
推迟&减少I/O操作减少动画图片组的数量,替换大图资源等。因为相比于内存操作,硬盘I/O是非常耗时的操作
推迟执行的一些任务如一些资源的I/O,一些布局逻辑,对象的创建时机等

八、优化串行操作

在冷启动过程中,有很多操作是串行执行的,若干个任务串行执行,时间必然比较长。如果能变串行为并行,那么冷启动时间就能够大大缩短。

闪屏页的使用

现在许多App在启动时并不直接进入首页,而是会向用户展示一个持续一小段时间的闪屏页,如果使用恰当,这个闪屏页就能帮我们节省一些启动时间。因为当一个App比较复杂的时候,启动时首次构建App的UI就是一个比较耗时的过程,假定这个时间是0.2秒,如果我们是先构建首页UI,然后再在Window上加上这个闪屏页,那么冷启动时,App就会实实在在地卡住0.2秒,但是如果我们是先把闪屏页作为App的RootViewController,那么这个构建过程就会很快。因为闪屏页只有一个简单的ImageView,而这个ImageView则会向用户展示一小段时间,这时我们就可以利用这一段时间来构建首页UI了,一举两得。

缓存定位&首页预请求

美团外卖App冷启动过程中一个重要的串行流程就是:首页定位–>首页请求–>首页渲染过程,这三个操作占了整个首页加载时间的77%左右,所以想要缩短冷启动时间,就一定要从这三点出发进行优化。

之前串行操作流程如下:

优化后的设计,在发起定位的同时,使用客户端缓存定位,进行首页数据的预请求,使定位和请求并行进行。然后当用户真实定位成功后,判断真实定位是否命中缓存定位,如果命中,则刚才的预请求数据有效,这样可以节省大概40%的时间首页加载时间,效果非常明显;如果未命中,则弃用预请求数据,重新请求。

九、数据监控

Time Profiler和Caesium火焰图都只能在线下分析App在单台设备中的耗时操作,局限性比较大,无法在线上监控App在用户设备上的表现。外卖App使用公司内部自研的Metrics性能监控系统,长期监控App的性能指标,帮助我们掌握App在线上各种环境下的真实表现,并为技术优化项目提供可靠的数据支持。Metrics监控的核心指标之一,就是冷启动时间。

冷启动开始&结束时间节点

  1. 结束时间点:结束时间比较好确定,我们可以将首页某些视图元素的展示作为首页加载完成的标志。
  2. 开始时间点:一般情况下,我们都是在main()之后才开始接管App,但以main()函数作为冷启动起始点显然不合适,因为这样无法统计到T1时间段。那么,起始时间如何确定呢?目前业界常见的有两种方法,一是以可执行文件中任意一个类的+load方法的执行时间作为起始点;二是分析dylib的依赖关系,找到叶子节点的dylib,然后以其中某个类的+load方法的执行时间作为起始点。根据Dyld对dylib的加载顺序,后者的时机更早。但是这两种方法获取的起始点都只在Initializers阶段,而Initializers之前的时长都没有被计入。Metrics则另辟蹊径,以App的进程创建时间(即exec函数执行时间)作为冷启动的起始时间。因为系统允许我们通过sysctl函数获得进程的有关信息,其中就包括进程创建的时间戳。
#import <sys/sysctl.h>
#import <mach/mach.h>

+ (BOOL)processInfoForPID:(int)pid procInfo:(struct kinfo_proc*)procInfo
{
    int cmd[4] = {CTL_KERN, KERN_PROC, KERN_PROC_PID, pid};
    size_t size = sizeof(*procInfo);
    return sysctl(cmd, sizeof(cmd)/sizeof(*cmd), procInfo, &size, NULL, 0) == 0;
}

+ (NSTimeInterval)processStartTime
{
    struct kinfo_proc kProcInfo;
    if ([self processInfoForPID:[[NSProcessInfo processInfo] processIdentifier] procInfo:&kProcInfo]) {
        return kProcInfo.kp_proc.p_un.__p_starttime.tv_sec * 1000.0 + kProcInfo.kp_proc.p_un.__p_starttime.tv_usec / 1000.0;
    } else {
        NSAssert(NO, @"无法取得进程的信息");
        return 0;
    }
}

进程创建的时机非常早。经过实验,在一个新建的空白App中,进程创建时间比叶子节点dylib中的+load方法执行时间早12ms,比main函数的执行时间早13ms(实验设备:iPhone 7 Plus (iOS 12.0)、Xcode 10.0、Release 模式)。外卖App线上的数据则更加明显,同样的机型(iPhone 7 Plus)和系统版本(iOS 12.0),进程创建时间比叶子节点dylib中的+load方法执行时间早688ms。而在全部机型和系统版本中,这一数据则是878ms。

冷启动过程时间节点

我们也在App冷启动过程中的所有关键节点打上一连串测速点,Metrics会记录下测速点的名称,及其距离进程创建时间的时长。我们没有采用自动打点的方式,是因为外卖App的冷启动过程十分复杂,而自动打点无法做到如此细致,并不实用。另外,Metrics记录的是时间轴上以进程创建时间为原点的一组顺序的时间点,而不是一组时间段,是因为顺序的时间点可以计算任意两个时间点之间的距离,即可以将时间点处理成时间段。但是,一组时间段可能无法还原为顺序的时间点,因为时间段之间可能并不是首尾相接的,特别是对于异步执行或者多线程的情况。

在测速完毕后,Metrics会统一将所有测速点上报到后台。下图是美团外卖App 6.10版本的部分过程节点监控数据截图:

Metrics还会由后台对数据做聚合计算,得到冷启动总时长和各个测速点时长的50分位数、90分位数和95分位数的统计数据,这样我们就能从宏观上对冷启动时长分布情况有所了解。下图中横轴为时长,纵轴为上报的样本数。

十、总结

对于快速迭代的App,随着业务复杂度的增加,冷启动时长会不可避免的增加。冷启动流程也是一个比较复杂的过程,当遇到冷启动性能瓶颈时,我们可以根据App自身的特点,配合工具的使用,从多方面、多角度进行优化。同时,优化冷启动存量问题只是冷启动治理的第一步,因为冷启动性能问题并不是一日造成的,也不能简单的通过一次优化工作就能解决,我们需要通过合理的设计、规范的约束,来有效地管控性能问题的增量,并通过持续的线上监控来及时发现并修正性能问题,这样才能够长期保证良好的App冷启动体验。

作者简介

郭赛,美团点评资深工程师。2015年加入美团,目前作为外卖iOS团队主力开发,负责移动端业务开发,业务类基础设施的建设与维护。

徐宏,美团点评资深工程师。2016年加入美团,目前作为外卖iOS团队主力开发,负责移动端APM性能监控,高可用基础设施支撑相关推进工作。

来自:https://tech.meituan.com/waimai_ios_optimizing_startup.html

数据库智能运维探索与实践(转载)

从自动化到智能化运维过渡时,美团DBA团队进行了哪些思考、探索与实践?本文根据赵应钢在“第九届中国数据库技术大会”上的演讲内容整理而成,部分内容有更新。

背景

近些年,传统的数据库运维方式已经越来越难于满足业务方对数据库的稳定性、可用性、灵活性的要求。随着数据库规模急速扩大,各种NewSQL系统上线使用,运维逐渐跟不上业务发展,各种矛盾暴露的更加明显。在业务的驱动下,美团点评DBA团队经历了从“人肉”运维到工具化、产品化、自助化、自动化的转型之旅,也开始了智能运维在数据库领域的思考和实践。

本文将介绍美团点评整个数据库平台的演进历史,以及我们当前的情况和面临的一些挑战,最后分享一下我们从自动化到智能化运维过渡时,所进行的思考、探索与实践。

数据库平台的演变

我们数据库平台的演进大概经历了五个大的阶段:

第一个是脚本化阶段,这个阶段,我们人少,集群少,服务流量也比较小,脚本化的模式足以支撑整个服务。

第二个是工具化阶段,我们把一些脚本包装成工具,围绕CMDB管理资产和服务,并完善了监控系统。这时,我们的工具箱也逐渐丰富起来,包括DDL变更工具、SQL Review工具、慢查询采集分析工具和备份闪回工具等等。

第三个是产品化阶段,工具化阶段可能还是单个的工具,但是在完成一些复杂操作时,就需要把这些工具组装起来形成一个产品。当然,并不是说这个产品一定要做成Web系统的形式,而是工具组装起来形成一套流程之后,就可以保证所有DBA的操作行为,对流程的理解以及对线上的影响都是一致的。我们会在易用性和安全性层面不断进行打磨。而工具产品化的主要受益者是DBA,其定位是提升运维服务的效率,减少事故的发生,并方便进行快速统一的迭代。

第四个是打造私有云平台阶段,随着美团点评业务的高速发展,仅靠十几、二十个DBA越来越难以满足业务发展的需要。所以我们就把某些日常操作开放授权,让开发人员自助去做,将DBA从繁琐的操作中解放出来。当时整个平台每天执行300多次改表操作;自助查询超过1万次;自助申请账号、授权并调整监控;自助定义敏感数据并授权给业务方管理员自助审批和管理;自定义业务的高峰和低峰时间段等等;自助下载、查询日志等等。

第五个是自动化阶段,对这个阶段的理解,其实是“仁者见仁,智者见智”。大多数人理解的自动化,只是通过Web平台来执行某些操作,但我们认为这只是半自动化,所谓的自动化应该是完全不需要人参与。目前,我们很多操作都还处于半自动化阶段,下一个阶段我们需要从半自动过渡到全自动。以MySQL系统为例,从运维角度看包括主从的高可用、服务过载的自我保护、容量自动诊断与评估以及集群的自动扩缩容等等。

现状和面临的挑战

下图是我们平台的现状,以关系数据库RDS平台为例,其中集成了很多管理的功能,例如主从的高可用、MGW的管理、DNS的变更、备份系统、升级流程、流量分配和切换系统、账号管理、数据归档、服务与资产的流转系统等等。

而且我们按照逻辑对平台设计进行了划分,例如以用户维度划分的RDS自助平台,DBA管理平台和测试环境管理平台;以功能维度划分的运维、运营和监控;以存储类型为维度划分的关系型数据库MySQL、分布式KV缓存、分布式KV存储,以及正在建设中的NewSQL数据库平台等等。未来,我们希望打造成“MySQL+NoSQL+NewSQL,存储+缓存的一站式服务平台”。

挑战一:RootCause定位难

即便我们打造了一个很强大的平台,但还是发现有很多问题难以搞定。第一个就是故障定位,如果是简单的故障,我们有类似天网、雷达这样的系统去发现和定位。但是如果故障发生在数据库内部,那就需要专业的数据库知识,去定位和查明到底是什么原因导致了故障。

通常来讲,故障的轨迹是一个链,但也可能是一个“多米诺骨牌”的连环。可能因为一些原因导致SQL执行变慢,引起连接数的增长,进而导致业务超时,而业务超时又会引发业务不断重试,结果会产生更多的问题。当我们收到一个报警时,可能已经过了30秒甚至更长时间,DBA再去查看时,已经错过了最佳的事故处理时机。所以,我们要在故障发生之后,制定一些应对策略,例如快速切换主库、自动屏蔽下线问题从库等等。除此之外,还有一个比较难的问题,就是如何避免相似的故障再次出现。

挑战二:人力和发展困境

第二个挑战是人力和发展的困境,当服务流量成倍增长时,其成本并不是以相同的速度对应增长的。当业务逻辑越来越复杂时,每增加一块钱的营收,其后面对应的数据库QPS可能是2倍甚至5倍,业务逻辑越复杂,服务支撑的难度越大。另外,传统的关系型数据库在容量、延时、响应时间以及数据量等方面很容易达到瓶颈,这就需要我们不断拆分集群,同时开发诉求也多种多样,当我们尝试使用平台化的思想去解决问题时,还要充分思考如何满足研发人员多样化的需求。

人力困境这一问题,从DBA的角度来说,时间被严重的碎片化,自身的成长就会遇到瓶颈,比如经常会做一些枯燥的重复操作;另外,业务咨询量暴增,尽管我们已经在尝试平台化的方法,但是还是跟不上业务发展的速度。还有一个就是专业的DBA越来越匮乏,越来越贵,关键是根本招聘不到人手。

在这种背景下,我们必须去思考:如何突破困局?如何朝着智能化转型?传统运维苦在哪里?智能化运维又能解决哪些问题?

首先从故障产生的原因来说,传统运维是故障触发,而智能运维是隐患驱动。换句话来说,智能运维不用报警,通过看报表就能知道可能要出事了,能够把故障消灭在“萌芽”阶段;第二,传统运维是被动接受,而智能运维是主动出击。但主动出击不一定是通过DBA去做,可能是系统或者机器人操作;第三,传统运维是由DBA发起和解决的,而智能运维是系统发起、RD自助;第四,传统运维属于“人肉救火”,而智能运维属于“智能决策执行”;最后一点,传统运维需要DBA亲临事故现场,而智能运维DBA只需要“隐身幕后”。

从自动化到智能化

那么,如何从半自动化过渡到自动化,进而发展到智能化运维呢?在这个过程中,我们会面临哪些痛点呢?

我们的目标是为整个公司的业务系统提供高效、稳定、快速的存储服务,这也是DBA存在的价值。业务并不关心后面是MySQL还是NoSQL,只关心数据是否没丢,服务是否可用,出了问题之后多长时间能够恢复等等。所以我们尽可能做到把这些东西对开发人员透明化,提供稳定高效快速的服务。而站在公司的角度,就是在有限的资源下,提升效率,降低成本,尽可能长远地解决问题。

上图是传统运维和智能运维的特点分析,左边属于传统运维,右边属于智能运维。传统运维在采集这一块做的不够,所以它没有太多的数据可供参考,其分析和预警能力是比较弱的。而智能运维刚好是反过来,重采集,很多功夫都在平时做了,包括分析、预警和执行,智能分析并推送关键报表。

而我们的目标,是让智能运维中的“报警+分析+执行”的比重占据的越来越少。

决策执行如何去做呢?我们都知道,预警重要但不紧急,但报警是紧急且重要的,如果你不能够及时去处理的话,事态可能会扩大,甚至会给公司带来直接的经济损失。

预警通常代表我们已经定位了一个问题,它的决策思路是非常清晰的,可以使用基于规则或AI的方式去解决,相对难度更小一些。而报警依赖于现场的链路分析,变量多、路径长,所以决策难,间接导致任何决策的风险可能都变大。所以说我们的策略就是全面的采集数据,然后增多预警,率先实现预警发现和处理的智能化。就像我们既有步枪,也有手枪和刺刀,能远距离解决敌人的,就尽量不要短兵相接、肉搏上阵。

数据采集,从数据库角度来说,我们产生的数据分成四块,Global Status、Variable,Processlist、InnoDB Status,Slow、Error、General Log和Binlog;从应用侧来说,包含端到端成功率、响应时间95线、99线、错误日志和吞吐量;从系统层面,支持秒级采样、操作系统各项指标;从变更侧来看,包含集群拓扑调整、在线DDL、DML变更、DB平台操作日志和应用端发布记录等等。

数据分析,首先是围绕集群分析,接着是实例、库,最后是表,其中每个对象都可以在多项指标上同比和环比,具体对比项可参考上图。

通过上面的步骤,我们基本可以获得数据库的画像,并且帮助我们从整体上做资源规划和服务治理。例如,有些集群实例数特别多且有继续增加的趋势,那么服务器需要scale up;读增加迅猛,读写比变大,那么应考虑存储KV化;利用率和分布情况会影响到服务器采购和预算制定;哪几类报警最多,就专项治理,各个击破。

从局部来说,我们根据分析到的一些数据,可以做一个集群的健康体检,例如数据库的某些指标是否超标、如何做调整等等。

数据库预警,通过分析去发现隐患,把报警转化为预警。上图是我们实际情况下的报警统计分析结果,其中主从延迟占比最大。假设load.1minPerCPU比较高,我们怎么去解决?那么,可能需要采购CPU单核性能更高的机器,而不是采用更多的核心。再比如说磁盘空间,当我们发现3T的磁盘空间普遍不够时,我们下次可以采购6T或更大空间的磁盘。

针对空间预警问题,什么时候需要拆分集群?MySQL数据库里,拆分或迁移数据库,花费的时间可能会很久。所以需要评估当前集群,按目前的增长速度还能支撑多长时间,进而反推何时要开始拆分、扩容等操作。

针对慢查询的预警问题,我们会统计红黑榜,上图是统计数据,也有利用率和出轨率的数据。假设这是一个金融事业群的数据库,假设有业务需要访问且是直连,那么这时就会产生几个问题:第一个,有没有数据所有者的授权;第二个,如果不通过服务化方式或者接口,发生故障时,它可能会导致整个金融的数据库挂,如何进行降级?所以,我们会去统计出轨率跟慢查询,如果某数据库正被以一种非法的方式访问,那么我们就会扫描出来,再去进行服务治理。

从运维的层面来说,我们做了故障快速转移,包括自动生成配置文件,自动判断是否启用监控,切换后自动重写配置,以及从库可自动恢复上线等等。

报警自动处理,目前来说大部分的处理工作还是基于规则,在大背景下拟定规则,触发之后,按照满足的前提条件触发动作,随着库的规则定义的逐渐完善和丰富,可以逐步解决很多简单的问题,这部分就不再需要人的参与。

展望

未来我们还会做一个故障诊断平台,类似于“扁鹊”,实现日志的采集、入库和分析,同时提供接口,供全链路的故障定位和分析、服务化治理。

展望智能运维,应该是在自动化和智能化上交叠演进,在ABC(AI、Big Data、Cloud Computing)三个方向上深入融合。在数据库领域,NoSQL和SQL界限正变得模糊,软硬结合、存储计算分离架构也被越来越多的应用,智能运维正当其时,我们也面临更多新的挑战。我们的目标是,希望通过DB平台的不断建设加固,平台能自己发现问题,自动定位问题,并智能的解决问题。

作者简介

应钢,美团点评研究员,数据库专家。曾就职于百度、新浪、去哪儿网等,10年数据库自动化运维开发、数据库性能优化、大规模数据库集群技术保障和架构优化经验。精通主流的SQL与NoSQL系统,现专注于公司业务在NewSQL领域的创新和落地。

来自:https://tech.meituan.com/Intelligent_Operation_Practice_in_meituan.html

LruCache在美团DSP系统中的应用演进(转载)

背景

DSP系统是互联网广告需求方平台,用于承接媒体流量,投放广告。业务特点是并发度高,平均响应低(百毫秒)。

为了能够有效提高DSP系统的性能,美团平台引入了一种带有清退机制的缓存结构LruCache(Least Recently Used Cache),在目前的DSP系统中,使用LruCache + 键值存储数据库的机制将远端数据变为本地缓存数据,不仅能够降低平均获取信息的耗时,而且通过一定的清退机制,也可以维持服务内存占用在安全区间。

本文将会结合实际应用场景,阐述引入LruCache的原因,并会在高QPS下的挑战与解决方案等方面做详细深入的介绍,希望能对DSP感兴趣的同学有所启发。

LruCache简介

LruCache采用的缓存算法为LRU(Least Recently Used),即最近最少使用算法。这一算法的核心思想是当缓存数据达到预设上限后,会优先淘汰近期最少使用的缓存对象。

LruCache内部维护一个双向链表和一个映射表。链表按照使用顺序存储缓存数据,越早使用的数据越靠近链表尾部,越晚使用的数据越靠近链表头部;映射表通过Key-Value结构,提供高效的查找操作,通过键值可以判断某一数据是否缓存,如果缓存直接获取缓存数据所属的链表节点,进一步获取缓存数据。LruCache结构图如下所示,上半部分是双向链表,下半部分是映射表(不一定有序)。双向链表中value_1所处位置为链表头部,value_N所处位置为链表尾部。

LruCache 初始结构

LruCache读操作,通过键值在映射表中查找缓存数据是否存在。如果数据存在,则将缓存数据所处节点从链表中当前位置取出,移动到链表头部;如果不存在,则返回查找失败,等待新数据写入。下图为通过LruCache查找key_2后LruCache结构的变化。

LruCache 查找

LruCache没有达到预设上限情况下的写操作,直接将缓存数据加入到链表头部,同时将缓存数据键值与缓存数据所处的双链表节点作为键值对插入到映射表中。下图是LruCache预设上限大于N时,将数据M写入后的数据结构。

LruCache 未达预设上限,添加数据

LruCache达到预设上限情况下的写操作,首先将链表尾部的缓存数据在映射表中的键值对删除,并删除链表尾部数据,再将新的数据正常写入到缓存中。下图是LruCache预设上限为N时,将数据M写入后的数据结构。

LruCache 达预设上限,添加数据

线程安全的LruCache在读写操作中,全部使用锁做临界区保护,确保缓存使用是线程安全的。

LruCache在美团DSP系统的应用场景

在美团DSP系统中广泛应用键值存储数据库,例如使用Redis存储广告信息,服务可以通过广告ID获取广告信息。每次请求都从远端的键值存储数据库中获取广告信息,请求耗时非常长。随着业务发展,QPS呈现巨大的增长趋势,在这种高并发的应用场景下,将广告信息从远端键值存储数据库中迁移到本地以减少查询耗时是常见解决方案。另外服务本身的内存占用要稳定在一个安全的区间内。面对持续增长的广告信息,引入LruCache + 键值存储数据库的机制来达到提高系统性能,维持内存占用安全、稳定的目标。

LruCache + Redis机制的应用演进

在实际应用中,LruCache + Redis机制实践分别经历了引入LruCache、LruCache增加时效清退机制、HashLruCache满足高QPS应用场景以及零拷贝机制四个阶段。各阶段的测试机器是16核16G机器。

演进一:引入LruCache提高美团DSP系统性能

在较低QPS环境下,直接请求Redis获取广告信息,可以满足场景需求。但是随着单机QPS的增加,直接请求Redis获取广告信息,耗时也会增加,无法满足业务场景的需求。

引入LruCache,将远端存放于Redis的信息本地化存储。LruCache可以预设缓存上限,这个上限可以根据服务所在机器内存与服务本身内存占用来确定,确保增加LruCache后,服务本身内存占用在安全范围内;同时可以根据查询操作统计缓存数据在实际使用中的命中率。

下图是增加LruCache结构前后,且增加LruCache后命中率高于95%的情况下,针对持续增长的QPS得出的数据获取平均耗时(ms)对比图:

引入LruCache前后平均耗时

根据平均耗时图显示可以得出结论:

  1. QPS高于250后,直接请求Redis获取数据的平均耗时达到10ms以上,完全无法满足使用的需求。
  2. 增加LruCache结构后,耗时下降一个量级。从平均耗时角度看,QPS不高于500的情况下,耗时低于2ms。

下图是增加LruCache结构前后,且增加LruCache后命中率高于95%的情况下,针对持续增长的QPS得出的数据获取Top999耗时(ms)对比图:

引入LruCache前后tp999耗时

根据Top999耗时图可以得出以下结论:

  1. 增加LruCache结构后,Top999耗时比平均耗时增长一个数量级。
  2. 即使是较低的QPS下,使用LruCache结构的Top999耗时也是比较高的。

引入LruCache结构,在实际使用中,在一定的QPS范围内,确实可以有效减少数据获取的耗时。但是QPS超出一定范围后,平均耗时和Top999耗时都很高。所以LruCache在更高的QPS下性能还需要进一步优化。

演进二:LruCache增加时效清退机制

在业务场景中,Redis中的广告数据有可能做修改。服务本身作为数据的使用方,无法感知到数据源的变化。当缓存的命中率较高或者部分数据在较长时间内多次命中,可能出现数据失效的情况。即数据源发生了变化,但服务无法及时更新数据。针对这一业务场景,增加了时效清退机制。

时效清退机制的组成部分有三点:设置缓存数据过期时间,缓存数据单元增加时间戳以及查询中的时效性判断。缓存数据单元将数据进入LruCache的时间戳与数据一起缓存下来。缓存过期时间表示缓存单元缓存的时间上限。查询中的时效性判断表示查询时的时间戳与缓存时间戳的差值超过缓存过期时间,则强制将此数据清空,重新请求Redis获取数据做缓存。

在查询中做时效性判断可以最低程度的减少时效判断对服务的中断。当LruCache预设上限较低时,定期做全量数据清理对于服务本身影响较小。但如果LruCache的预设上限非常高,则一次全量数据清理耗时可能达到秒级甚至分钟级,将严重阻断服务本身的运行。所以将时效性判断加入到查询中,只对单一的缓存单元做时效性判断,在服务性能和数据有效性之间做了折中,满足业务需求。

演进三:高QPS下HashLruCache的应用

LruCache引入美团DSP系统后,在一段时间内较好地支持了业务的发展。随着业务的迭代,单机QPS持续上升。在更高QPS下,LruCache的查询耗时有了明显的提高,逐渐无法适应低平响的业务场景。在这种情况下,引入了HashLruCache机制以解决这个问题。

LruCache在高QPS下的耗时增加原因分析:

线程安全的LruCache中有锁的存在。每次读写操作之前都有加锁操作,完成读写操作之后还有解锁操作。在低QPS下,锁竞争的耗时基本可以忽略;但是在高QPS下,大量的时间消耗在了等待锁的操作上,导致耗时增长。

HashLruCache适应高QPS场景:

针对大量的同步等待操作导致耗时增加的情况,解决方案就是尽量减小临界区。引入Hash机制,对全量数据做分片处理,在原有LruCache的基础上形成HashLruCache,以降低查询耗时。

HashLruCache引入某种哈希算法,将缓存数据分散到N个LruCache上。最简单的哈希算法即使用取模算法,将广告信息按照其ID取模,分散到N个LruCache上。查询时也按照相同的哈希算法,先获取数据可能存在的分片,然后再去对应的分片上查询数据。这样可以增加LruCache的读写操作的并行度,减小同步等待的耗时。

下图是使用16分片的HashLruCache结构前后,且命中率高于95%的情况下,针对持续增长的QPS得出的数据获取平均耗时(ms)对比图:

引入HashLruCache前后平均耗时

根据平均耗时图可以得出以下结论:

  1. 使用HashLruCache后,平均耗时减少将近一半,效果比较明显。
  2. 对比不使用HashLruCache的平均耗时可以发现,使用HashLruCache的平均耗时对QPS的增长不敏感,没有明显增长。

下图是使用16分片的HashLruCache结构前后,且命中率高于95%的情况下,针对持续增长的QPS得出的数据获取Top999耗时(ms)对比图:

引入HashLruCache前后tp999耗时

根据Top999耗时图可以得出以下结论:

  1. 使用HashLruCache后,Top999耗时减少为未使用时的三分之一左右,效果非常明显。
  2. 使用HashLruCache的Top999耗时随QPS增长明显比不使用的情况慢,相对来说对QPS的增长敏感度更低。

引入HashLruCache结构后,在实际使用中,平均耗时和Top999耗时都有非常明显的下降,效果非常显著。

HashLruCache分片数量确定:

根据以上分析,进一步提高HashLruCache性能的一个方法是确定最合理的分片数量,增加足够的并行度,减少同步等待消耗。所以分片数量可以与CPU数量一致。由于超线程技术的使用,可以将分片数量进一步提高,增加并行性。

下图是使用HashLruCache机制后,命中率高于95%,不同分片数量在不同QPS下得出的数据获取平均耗时(ms)对比图:

HashLruCache分片数量耗时

平均耗时图显示,在较高的QPS下,平均耗时并没有随着分片数量的增加而有明显的减少,基本维持稳定的状态。

下图是使用HashLruCache机制后,命中率高于95%,不同分片数量在不同QPS下得出的数据获取Top999耗时(ms)对比图:

HashLruCache分片tp99耗时

Top999耗时图显示,QPS为750时,分片数量从8增长到16再增长到24时,Top999耗时有一定的下降,并不显著;QPS为1000时,分片数量从8增长到16有明显下降,但是从16增长到24时,基本维持了稳定状态。明显与实际使用的机器CPU数量有较强的相关性。

HashLruCache机制在实际使用中,可以根据机器性能并结合实际场景的QPS来调节分片数量,以达到最好的性能。

演进四:零拷贝机制

线程安全的LruCache内部维护一套数据。对外提供数据时,将对应的数据完整拷贝一份提供给调用方使用。如果存放结构简单的数据,拷贝操作的代价非常小,这一机制不会成为性能瓶颈。但是美团DSP系统的应用场景中,LruCache中存放的数据结构非常复杂,单次的拷贝操作代价很大,导致这一机制变成了性能瓶颈。

理想的情况是LruCache对外仅仅提供数据地址,即数据指针。使用方在业务需要使用的地方通过数据指针获取数据。这样可以将复杂的数据拷贝操作变为简单的地址拷贝,大量减少拷贝操作的性能消耗,即数据的零拷贝机制。直接的零拷贝机制存在安全隐患,即由于LruCache中的时效清退机制,可能会出现某一数据已经过期被删除,但是使用方仍然通过持有失效的数据指针来获取该数据。

进一步分析可以确定,以上问题的核心是存放于LruCache的数据生命周期对于使用方不透明。解决这一问题的方案是为LruCache中存放的数据添加原子变量的引用计数。使用原子变量不仅确保了引用计数的线程安全,使得各个线程读取的引用计数一致,同时保证了并发状态最小的同步性能开销。不论是LruCache中还是使用方,每次获取数据指针时,即将引用计数加1;同理,不再持有数据指针时,引用计数减1。当引用计数为0时,说明数据没有被任何使用方使用,且数据已经过期从LruCache中被删除。这时删除数据的操作是安全的。

下图是使零拷贝机制后,命中率高于95%,不同QPS下得出的数据获取平均耗时(ms)对比图:

HashLruCache分片数量耗时

平均耗时图显示,使用零拷贝机制后,平均耗时下降幅度超过60%,效果非常显著。

下图是使零拷贝机制后,命中率高于95%,不同QPS下得出的数据获取Top999耗时(ms)对比图:

HashLruCache分片数量耗时

根据Top999耗时图可以得出以下结论:

  1. 使用零拷贝后,Top999耗时降幅将近50%,效果非常明显。
  2. 在高QPS下,使用零拷贝机制的Top999耗时随QPS增长明显比不使用的情况慢,相对来说对QPS的增长敏感度更低。

引入零拷贝机制后,通过拷贝指针替换拷贝数据,大量降低了获取复杂业务数据的耗时,同时将临界区减小到最小。线程安全的原子变量自增与自减操作,目前在多个基础库中都有实现,例如C++11就提供了内置的整型原子变量,实现线程安全的自增与自减操作。

在HashLruCache中引入零拷贝机制,可以进一步有效降低平均耗时和Top999耗时,且在高QPS下对于稳定Top999耗时有非常好的效果。

总结

下图是一系列优化措施前后,命中率高于95%,不同QPS下得出的数据获取平均耗时(ms)对比图:

HashLruCache分片数量耗时

平均耗时图显示,优化后的平均耗时仅为优化前的20%以内,性能提升非常明显。优化后平均耗时对于QPS的增长敏感度更低,更好的支持了高QPS的业务场景。

下图是一系列优化措施前后,命中率高于95%,不同QPS下得出的数据获取Top999耗时(ms)对比图:

HashLruCache分片数量耗时

Top999耗时图显示,优化后的Top999耗时仅为优化前的20%以内,对于长尾请求的耗时有非常明显的降低。

LruCache是一个非常常见的数据结构。在美团DSP的高QPS业务场景下,发挥了重要的作用。为了符合业务需要,在原本的清退机制外,补充了时效性强制清退机制。随着业务的发展,针对更高QPS的业务场景,使用HashLruCache机制,降低缓存的查询耗时。针对不同的具体场景,在不同的QPS下,不断尝试更合理的分片数量,不断提高HashLruCache的查询性能。通过引用计数的方案,在HashLruCache中引入零拷贝机制,进一步大幅降低平均耗时和Top999耗时,更好的服务于业务场景的发展。

作者简介

王粲,2018年11月加入美团,任职美团高级工程师,负责美团DSP系统后端基础架构的研发工作。

崔涛,2015年6月加入美团,任职资深广告技术专家,期间一手指导并从0到1搭建美团DSP投放平台,具备丰富的大规模计算引擎的开发和性能优化经验。

霜霜,2015年6月加入美团,任职美团高级工程师,美团DSP系统后端基础架构与机器学习架构负责人,全面负责DSP业务广告召回和排序服务的架构设计与优化。文章来自:https://tech.meituan.com/lrucache_practice_dsp.html

AI技术在智能海报设计中的应用(转载)

背景

在视觉设计领域中,设计师们往往会因为一些简单需求付出相当多的时间,比如修改文案内容,设计简单的海报版式,针对不同机型、展位的多尺寸拓展等。这些工作需要耗费大量的时间、人力成本(5~6张/人日),但对设计师的进步成长起到的作用却非常有限。另一方面,精准营销是未来的大趋势,在大流量背景下,首页的海报资源展位需要展示“千人千面”的效果,这对海报的生产效率也提出了非常高的要求。所以,我们美团外卖技术团队尝试结合AI技术,来协助设计师避免这种低收益、高重复的任务,同时低成本、高效率、高质量地完成海报图片的生成。本文以Banner(横版海报)为例,介绍我们在海报设计AI技术结合方面所进行的一些探索和研究。

分析

什么是Banner的设计过程?我们尝试总结了对Banner设计的理解,Banner的设计过程是一系列的具备某种特征属性的素材图层的有序叠加过程。这里的特征属性既包括颜色、形状、纹理、主题等视觉属性,也包括位置、大小、贴边等空间属性。在这个过程中,哪些环节可以被机器算法所探索呢?文献[1]研究了如何调整图像的颜色分布,使杂志封面的视觉效果更加符合人眼的视觉特性;文献[2]以此为基础,引入了基于显著性识别的图像裁剪,并使用优化方法来解决布局问题。阿里巴巴的鹿班系统在去年双十一当天,生成1.7亿张Banner;京东内部也在孵化玲珑和莎士比亚系统,更加智能地设计文案和Banner。

图1 封面配色&布局设计[2]

在设计领域的一些子问题上,可以用算法来挖掘出数据背后的规律(如图1所示)。那么,能否构建一个完整的学习算法和处理系统,统一解决Banner设计中所有的子问题(配色、布局、搭配、生成)呢?

技术方案

素材图层是Banner的基础元素,其本身可以被特征化,同时组成Banner的若干元素间的叠加顺序可以被序列化,因此,算法实际是在学习“在什么时候,选择某种素材,放在哪里”。

图2 流程框架

如图2所示,为了解决这个问题,我们设计规划器、优化器、生成器来共同构建海报设计的学习与生产过程。其中:

  1. 规划器从数据中学习设计师对不同风格下的设计习惯与规律;
  2. 优化器基于美学质量和设计原则,对前者的输出结果做精细化调整;
  3. 最后,由生成器选取/生成素材并渲染成图;
  4. 素材库作为这三个环节的基础,负责素材管理素材标签化

素材库

如何提取素材图片的特征属性,这是比较典型的分类问题。在计算机视觉领域,传统方案是提取图像的颜色、梯度等低级语义特征[3],结合传统的分类器(LR、SVM等)来实现分类。近年来,基于深度学习的方法因为能表达更为复杂的语义特征,逐渐成为主流方法[4]。如图3所示我们提取传统的低级语义特征,以及基于CNN的高级语义特征,来共同完成素材特征属性提取

图3 素材库-特征提取

规划器

完成素材的数据化工作后,怎样学习Banner的设计过程?

作为一种生成模型,对抗生成网络(GAN)[5]在近年广为应用,其优势是可以端到端地训练图像生成模型,但在我们的应用场景下,GAN存在以下两个问题:

  1. GAN的过程更像是“黑盒”的过程:输入方面,虽然Conditional-GAN之类的方法可以实现某种程度有条件地可控生成,但对于Banner设计任务来说,其输入信息(文案、目标风格、主体信息)仍然过于复杂;
  2. 输出方面,GAN直接生成源数据(即图像),但非常缺乏解释性。我们需要的是更加直观、更有解释性的信息,比如素材的类型、颜色、轮廓、位置等。

在上文中有提到,Banner设计过程是素材图层依次叠加的过程。因此,我们可以用序列生成模型来拟合这个过程[6]。在建模过程中,我们把素材视作词汇(Word),海报视作句子(Sentence),词汇索引视为离散化的特征索引,素材叠加顺序就可以视为句子中的词顺序[7]。

图4 规划器-序列生成

图4是我们使用的序列生成模型,输入主体信息和目标风格,输出素材特征的序列。为了增强预测过程中多条路径结果的多样性,我们在监督性地训练每个时刻的输出之外,还引入了评估整个序列合理性的Object loss。如图5所示,借鉴SeqGAN的思想,Object loss可以由判别器来提供[8]。

图5 SeqGAN[8]

优化器

规划器预测素材的量化特征,为了确保最终成图符合美学标准,需要一个后处理的过程(图6)。我们用优化器来解决这个问题。从本质上讲,这是一个优化过程。通过和设计师们的沟通,我们设计了一些基于常规设计理念和美学标准的目标函数,动作集合包括移动、缩放、亮度调整等,结合优化方法,提升Banner的视觉效果。

图6 优化器

生成器

优化后的素材特征序列,通过生成器来渲染成图。如图7所示,对于素材库检索不到符合某种特征属性的素材的情况,我们设计了图像风格迁移的方法来实现图像特征迁移。这里的特征可以是颜色、形状、纹理等低级特征,也可以是某种语义化的视觉风格特征,对后者来说,可以将源图像的内容Content和目标图像的风格Style在某种特征空间(CNN中的某一层)里做融合,实现风格迁移[9,10]。

图7 素材生成

应用场景及功能拓展

“千人千面”的精准营销是未来营销策略的趋势,这对商品素材的丰富性提出了非常高的要求;从为商家赋能的角度来说,也需要为商家提供更多样的海报版式,这也要求系统具备海报风格的快速学习能力和拓展能力。对此,在常规设计风格的研究之外,我们从以下3个方面做了一些拓展研究。

主体图片加工

商品素材的丰富度与美学质量是精细化营销及海报美学质量非常重要的一环。其中最核心的要求是图像分割的能力[11,12]。以全卷积网络(FCN)为基础,如图8所示,我们采取以下几个在目标分割领域常见的技巧,来实现商品图片的目标分割:

  1. Encoder-Decoder结构
  2. 空洞卷积
  3. 多尺度特征融合
  4. Two-Stage微调网络

图8 图像语义分割&抠图(结构图部分参考DeepLab v3+[12])

这种基于语义分割方法的结果,在专业设计师人工评审质量的过程中,发现主体边缘有时会出现明显的锯齿感。经过分析,我们认为有以下两个原因:

  1. 语义分割模型把问题建模为一个“像素级分类过程”,每一个像素的类别都天然地被假设为“非此即彼”,大多数的Segmentation模型都采用Cross-Entropy作为损失函数;
  2. 因此,无论是从模型结构(CNN)还是从损失函数的角度来说,分割模型会更倾向于全局信息从而丢弃局部结构信息,导致只能得到边缘比较粗糙的分割结果。

为此,如图8所示,我们在图像分割的输出结果之外,结合了Image Matting方法:

  1. 对分割模型的输出结果做形态学变换,生成三值Trimap,分别表示前景区、背景区、未知区;
  2. 应用常规的Matting方法,比如Bayesian、Close-Form等,以原图像和Trimap图像为输入,输出前景图的Alpha通道;
  3. Matting可以使前景主体的边缘更加平滑,视觉质量更高(图9)。

图9 商品主体抠图

另外,基于图像美学质量评分模型,我们会优先选择质量分高的图片作为主体素材来源。对中低分的图片,未来可以考虑借鉴Cycle-GAN[13]的思想,设计基于半监督和GAN的图像增强网络,美化后再经过图像分割产生主体素材。

海报模板拓展

上述的常规设计风格的学习属于一种广义的设计风格,需要设计师先期投入很多精力做风格划分以及数据收集、处理。为了快速适配热点场景,我们借鉴图像检索技术(如图10所示),提取素材图片的CNN特征及颜色特征,使用欧式距离来度量素材相似度,这能节省人工打标签的成本,实现基于固定模板的自动拓展及生成(海报临摹)。

图10 素材图像检索与模板拓展

多分辨率拓展

在日常工作中,设计师在设计出Banner后,往往要花费很长时间对不同展位、不同版本、不同机型做多尺寸适配(如图11所示)。能否用算法来协助人工提效?在素材已经确定,并且相互的位置关系近乎确定的条件下做多分辨率适配,这本质上是一个优化问题,我们在上文布局优化器的基础上,增加元素的局部相对位置与全局绝对位置的拓扑关系作为目标函数。目前,系统支持在某个固定宽高比±30%范围内的任意分辨率适配,未来会进一步扩大适配范围。

图11 多分辨率拓展

总结

目前,我们的Banner智能设计系统为钻展(外卖首页广告位)、商家店铺装修等业务提供稳定的设计能力;素材加工等子能力也在为外卖、闪购等商品图片提供技术支持。后续我们会从扩展常规设计风格、语义相关的颜色及素材挖掘、自动解析数据、构建自评估学习闭环等方面继续研究,进一步提高算法的设计能力和适用性,尽可能协助设计师提高效率,降低高重复性工作的时间和经济成本。

参考文献

[1] A. Jahanian, J. Liu, D. Tretter, Q. Lin, E. O’Brien-Strain, S. Lee, N. Lyons, and J. P. Allebach. “Automatic Design of Colors for Magazine Covers”. In Proc. IS&T/SPIE Electronic Imaging, International Society for Optics and Photonics, 2013
[2] X. Y. Yang, T. Mei, Y. Q. Xu, Y. Rui, S. P. Li. “Automatic Generation of Visual-Textual Presentation Layout”. ACM Transactions on Multimedia Computing, Communications, and Applications, 2017
[3] David G. Lowe. “Distinctive Image Features from Scale-Invariant Keypoints”. International journal of computer vision, 2004
[4] Alex Krizhevsky, Ilya Sutskever, Geoffrey E. Hinton. “ImageNet Classification with Deep Convolutional Neural Networks”. NIPS, 2012
[5] I. Goodfellow, J. Pouget-Abadie, M. Mirza, B. Xu, D. Warde-Farley, S. Ozair, A. Courville, Y. Bengio. “Generative Adversarial Networks”. NIPS, 2014
[6] K. Kawakami. “Supervised Sequence Labelling with Recurrent Neural Networks”. Studies in Computational Intelligence, 2008
[7] T. Mikolov. “Statistical Language Models based on Neural Networks”. 2012
[8] L. Yu, W. Zhang, J. Wang, Y. Yu. “SeqGAN: Sequence Generative Adversarial Nets with Policy Gradient”. AAAI, 2017
[9] L.A. Gatys, A.S. Ecker, M. Bethge. “Image Style Transfer Using Convolutional Neural Networks”. CVPR, 2016
[10] Y. Li, M.Y. Liu, X. Li, M.H. Yang, J. Kautz. “A Closed-form Solution to Photorealistic Image Stylization”. ECCV, 2018
[11] J. Long, E. Shelhamer, T. Darrell. “Fully Convolutional Networks for Semantic Segmentation”. CVPR, 2015
[12] L.C. Chen, Y. Zhu, G. Papandreou, F. Schroff, H. Adam. “Encoder-Decoder with Atrous Separable Convolution for Semantic Image Segmentation”. ECCV, 2018
[13] J.Y. Zhu, T. Park, P. Isola, A. A. Efros. “Unpaired Image-to-Image Translation using Cycle-Consistent Adversarial Networks”. ICCV, 2017

作者简介

晓星,2017年6月加入美团,目前主要负责美团外卖图像内容挖掘、增强、生成方面的相关工作,致力于图像相关技术的积累及落地。

文章来自:https://tech.meituan.com/AI_in_Banner_Design.html