携程技术2017年度合辑

baishikele

贡献于2018-03-01

字数:0 关键词:

[序] 这是我们第一次尝试把携程技术的相关文章整理成册,分享给技术圈的小伙伴们。 作为一家在上海的互联网企业,我们一直希望能营造起技术圈,尤其是上海技术 圈的交流氛围,让小伙伴们有一个更好的沟通平台。 这本小书的内容来自携程技术中心微信公众号在 2017 年发布的文章,共 66 篇, 25 万多字。包含了架构、大数据、前端、安全、运维、云计算、数据库等领域, 也包含了 2017 年八场携程技术沙龙上的干货内容。 这些文字背后,是携程近 3000 位技术人的支持,他们在日常繁忙的工作之余, 将自己在一线中的实践经验、踩过的坑毫无保留地分享出来。你也可以将它看成 是一本案例集锦。 希望这本小书,能带给你些许启发和帮助,在成长路上少走一些弯路。 也希望在新的 2018 年,每一位技术小伙伴都能成为更好的自己。 让我们一起加油,一起成长! 2018.1 上海 携程技术中心微信公号(ID:ctriptech) 携程技术中心官方账号,分享来自携程技术人的一手干货文章,发起线上线下技术活动, 发布热招岗位信息,和技术圈小伙伴一起学习成长~ 携程技术中心微信公号需要你的帮助~ 把你想看的内容告诉我们,我们给你一个最精彩的技术公号。 反馈戳这里:http://ctriptech.mikecrm.com/nbRt3Op 目录 架构篇 .....................................................................................................................................................................1 携程 Apollo 配置中心 ...............................................................................................................................2 携程 Redis 多数据中心解决方案 XPipe ..............................................................................................6 X-Series 企业级开发实践 ..................................................................................................................... 11 携程高可用架构的演变和迭代——应用开发者视角 ................................................................. 19 携程第四代架构探秘之运维基础架构升级 .................................................................................... 29 去哪儿消息消费模式 ............................................................................................................................. 50 Spring 探秘,妙用 BeanPostProcessor ........................................................................................... 58 携程在线风控系统架构 ......................................................................................................................... 65 分布式架构系统生成全局唯一序列号的一个思路 ...................................................................... 73 如何实现金服业务流程动态化 ........................................................................................................... 79 深入理解 Python 装饰器 ...................................................................................................................... 84 携程实时用户行为系统实践................................................................................................................ 89 大数据篇 ............................................................................................................................................................ 100 机器学习算法线上部署方法.............................................................................................................. 101 携程机票前台埋点二三事 .................................................................................................................. 106 携程机票的 ABTest 实践 ..................................................................................................................... 115 AAAI-2017 见闻 | 那些最牛逼的公司都在研究什么 .............................................................. 121 从底层到应用,那些数据人的必备技能....................................................................................... 127 ElasticSearch 相关性打分机制 .......................................................................................................... 133 携程机票大数据架构最佳实践 ......................................................................................................... 142 大规模知识图谱的构建、推理及应用 ........................................................................................... 155 如何基于 Spark Streaming 构建实时计算平台......................................................................... 162 无线篇 ................................................................................................................................................................ 172 从零打造携程无线持续交付平台 MCD 实践............................................................................. 173 携程小程序开发的那些事儿.............................................................................................................. 182 前端篇 ................................................................................................................................................................ 188 那些你不知道的爬虫反爬虫套路 .................................................................................................... 189 前端常用的通信技术 ........................................................................................................................... 199 长连接/websocket/SSE 等主流服务器推送技术比较 ............................................................... 217 如何一步步打造基于 React 的移动端 SPA 框架 ........................................................................ 227 安全篇 ................................................................................................................................................................ 246 携程是如何保障业务安全的.............................................................................................................. 247 图形验证码在携程的实践之路 ......................................................................................................... 256 携程安全自动化测试之路 .................................................................................................................. 264 携程业务风控数据平台建设.............................................................................................................. 270 ES 安全 searchguard 落地实践 ......................................................................................................... 275 机器学习在 web 攻击检测中的应用实践 ..................................................................................... 283 运维篇 ................................................................................................................................................................ 294 携程网基于应用的自动化容量管理与评估 .................................................................................. 295 携程运维工作流平台的演进之路 .................................................................................................... 306 携程新一代呼叫中心话务监控平台 ................................................................................................ 323 云计算篇 ............................................................................................................................................................ 335 携程容器云实践 ..................................................................................................................................... 336 携程容器云优化实践 ........................................................................................................................... 349 数据库篇 ............................................................................................................................................................ 365 MySQL 时间序列存储引擎的设计与实现 ..................................................................................... 366 一个 MySQL 5.7 分区表性能下降的案例分析 ............................................................................ 373 云服务篇 ............................................................................................................................................................ 380 揭秘携程基于融合通讯技术的新一代客服系统 ......................................................................... 381 携程呼叫中心移动坐席解决方案 .................................................................................................... 388 携程技术沙龙个性化推荐专场 .................................................................................................................. 395 饿了么推荐系统的从 0 到 1 .............................................................................................................. 396 腾讯云推荐引擎实践 ........................................................................................................................... 407 推荐系统中基于深度学习的混合协同过滤模型 ......................................................................... 413 跨领域推荐,实现个性化服务的技术途径 .................................................................................. 423 携程技术沙龙前端专场 ................................................................................................................................ 429 IMVC(同构 MVC)的前端实践 ..................................................................................................... 430 Qreact,去哪儿网的迷你 react 方案 ............................................................................................. 450 携程技术沙龙云海机器学习专场.............................................................................................................. 458 模型优化不得不思考的几个问题 .................................................................................................... 459 携程酒店浏览客户流失概率预测 .................................................................................................... 465 去哪儿酒店算法服务平台 .................................................................................................................. 476 机器学习在 1 号店商品匹配中的实践 ........................................................................................... 481 携程技术沙龙敏捷专场 ................................................................................................................................ 488 从四个案例看敏捷开发的持续改进 ................................................................................................ 489 携程火车票每年 N 倍增长背后,有哪些创新的管理方法 ..................................................... 497 携程技术沙龙移动开发专场 ....................................................................................................................... 512 MVP 模式在携程酒店的应用和扩展 .............................................................................................. 513 携程用户数据采集与分析系统 ......................................................................................................... 526 去哪儿网快速 App 开发及问题解决平台实践 .......................................................................... 537 携程技术沙龙测试专场 ................................................................................................................................ 550 去哪儿自动化测试框架 Qunit 中的零侵入切面技术应用及分布式运行平台 .................. 551 携程酒店 360 度 Java 质量控制 ....................................................................................................... 558 携程机票无线测试技术与效能提升 ................................................................................................ 565 携程技术沙龙架构专场 ................................................................................................................................ 578 携程开源配置中心 Apollo 的设计与实现 ..................................................................................... 579 去哪儿全链路跟踪及 Debug ............................................................................................................. 592 高吞吐消息网关的探索与思考 ......................................................................................................... 604 携程技术沙龙 AI 专场 .................................................................................................................................. 615 阿里小蜜-电商领域的智能助理技术实践 .................................................................................... 616 京东 JIMI 用户未来意图预测技术揭秘 .......................................................................................... 630 助理来也胡一川:深度学习在智能助理中的应用 .................................................................... 637 架构篇 1 架构篇 架构篇 2 携程 Apollo 配置中心 [作者简介]宋顺,携程框架研发部技术专家。2016 年初加入携程,主要负责中间件产品的相 关研发工作。毕业于复旦大学软件工程系,曾就职于大众点评,担任后台系统技术负责人。 随着程序功能的日益复杂,程序的配置日益增多:各种功能的开关、参数的配置、服务器的 地址…… 对程序配置的期望值也越来越高:配置修改后实时生效,分环境、分集群管理配置,完善的 权限、审核机制…… 在这样的大环境下,传统的通过配置文件、数据库等方式已经越来越无法满足开发人员对配 置管理的需求。 Apollo 配置中心应运而生! Apollo(阿波罗)是携程框架部门研发的配置管理平台,能够集中化管理应用不同环境、不 同集群的配置,配置修改后能够实时推送到应用端,并且具备规范的权限、流程治理等特性。 服务端基于 Spring Boot 和Spring Cloud 开发,打包后可以直接运行,不需要额外安装 Tomcat 等应用容器。 Java 客户端不依赖任何框架,能够运行于所有 Java 运行时环境,同时对 Spring 环境也有较 好的支持。 .Net 客户端不依赖任何框架,能够运行于所有.Net 运行时环境。 配置界面 架构篇 3 功能介绍 1、统一管理不同环境、不同集群的配置 Apollo 提供了一个统一界面集中式管理不同环境(environment)、不同集群(cluster)、不同 命名空间(namespace)的配置。 同一份代码部署在不同的集群,可以有不同的配置,比如 zk 的地址等 通过命名空间(namespace)可以很方便的支持多个不同应用共享同一份配置,同时还允许 应用对共享的配置进行覆盖 2、配置修改实时生效(热发布) 用户在 Apollo 修改完配置并发布后,客户端能实时(1 秒)接收到最新的配置,并通知到应 用程序。 3、版本发布管理 所有的配置发布都有版本概念,从而可以方便的支持配置的回滚。 4、灰度发布 支持配置的灰度发布,比如点了发布后,只对部分应用实例生效,等观察一段时间没问题后 架构篇 4 再推给所有应用实例。 5、权限管理、发布审核、操作审计 应用和配置的管理都有完善的权限管理机制,对配置的管理还分为了编辑和发布两个环节, 从而减少人为的错误。 所有的操作都有审计日志,可以方便的追踪问题。 6、客户端配置信息监控 可以方便的看到配置在被哪些实例使用 7、提供 Java 和.Net 原生客户端 提供了 Java 和.Net 的原生客户端,方便应用集成 支持 Spring Placeholder 和 Annotation,方便应用使用(需要 Spring 3.1.1+) 同时提供了 Http 接口,非 Java 和.Net 应用也可以方便的使用 8、提供开放平台 API Apollo 自身提供了比较完善的统一配置管理界面,支持多环境、多数据中心配置管理、权限、 流程治理等特性。 不过 Apollo 出于通用性考虑,对配置的修改不会做过多限制,只要符合基本的格式就能够 保存。 在我们的调研中发现,对于有些使用方,它们的配置可能会有比较复杂的格式,如 xml, json, 需要对格式做校验。 还有一些使用方如 DAL,不仅有特定的格式,而且对输入的值也需要进行校验后方可保存, 如检查数据库、用户名和密码是否匹配。 对于这类应用,Apollo 支持应用方通过开放接口在 Apollo 进行配置的修改和发布,并且具 备完善的授权和权限控制 9、部署简单 配置中心作为基础服务,可用性要求非常高,这就要求 Apollo 对外部依赖尽可能地少 目前唯一的外部依赖是 MySQL,所以部署非常简单,只要安装好 Java 和 MySQL 就可以让 Apollo 跑起来 架构篇 5 Apollo 还提供了打包脚本,一键就可以生成所有需要的安装包,并且支持自定义运行时参数 结语 目前 Apollo 在携程生产环境稳定服务着数千台机器的配置需求,同时也有多家外部公司投 入生产使用的成功案例。 大家如果对配置需求有痛点的话,建议可以关注一下 Apollo 配置中心,我们在 Github 上有 非常丰富的文档介绍,也有着一个非常活跃的技术支持群。 另外,Apollo 从项目之初就是作为一个开源项目开发的,目前开发/测试/产品/运维总共两 个人,所以也非常希望能有更多的力量投入进来,欢迎大家发起 Pull Request! 更多详情请参考:https://github.com/ctripcorp/apollo 架构篇 6 携程 Redis 多数据中心解决方案 XPipe [作者简介]孟文超,携程技术中心框架研发部高级经理。2016 年加入携程,目前主要负责 Redis 多数据中心项目 XPipe。此前曾在大众点评工作,任基础架构部门通信团队负责人。 Redis 在携程内部得到了广泛的使用,根据客户端数据统计,整个携程全部 Redis 的读写请 求在每秒 200W,其中写请求约每秒 10W,很多业务甚至会将 Redis 当成内存数据库使用。 这样,就对 Redis 多数据中心提出了很大的需求,一是为了提升可用性,解决数据中心 DR(DisasterRecovery)问题;二是提升访问性能,每个数据中心可以读取当前数据中心的数据, 无需跨机房读数据。在这样的需求下,XPipe 应运而生 。 从实现的角度来说,XPipe 主要需要解决三个方面的问题,一是数据复制,同时在复制的过 程中保证数据的一致性;二是高可用,XPipe 本身的高可用和 Redis 系统的高可用;三是如 何在机房异常时,进行 DR 切换。 下文将会从这三个方面对问题进行详细阐述。最后,将会对测试结果和系统在生产环境运行 情况进行说明。 为了方便描述,后面的行文中用 DC 代表数据中心(Data Center)。 一、数据复制问题 多数据中心首先要解决的是数据复制问题,即数据如何从一个 DC 传输到另外一个 DC,通 常有如下方案: 客户端双写 从客户端的角度来解决问题,单个客户端双写两个 DC 的服务器。初看没有什么问题。但是 深入看下去,如果写入一个 IDC 成功,另外一个 IDC 失败,那数据可能会不一致,为了保证 一致,可能需要先写入一个队列,然后再将队列的数据发送到两个 IDC。 如果队列是本地 队列,那当前服务器挂掉,数据可能会丢失;如果队列是远程队列,又给整体的方案带来了 很大的复杂度。 目前的应用一般都是集群部署,会有多个客户端同时操作。在多个客户端的前提下,又带来 了新的问题。比如两个客户端 ClientA 和 ClientB: ClientA: set key value1 ClientB: set key value2 由于两个客户端独立操作,到达服务器的顺序不可控,所以可能会导致两个 DC 的服务器对 架构篇 7 于同一个 key,value 不一致,如下: Server1: setkey value1; set key value2; Server2: setkey value2; set key value1; 在 Server1,最终值为 value2,在 Server2,最终值为 value1。 服务器代理 proxy 模式解决了多客户端写可能会导致数据不一致的问题。proxy 类似于一个 client,和单 个 client 双写的问题类似,需要一个数据队列保数据一致性。 为了提升系统的利用率,单个 proxy 往往需要代理多个 Redis server,如果 proxy 出问题, 会导致大面积的系统故障。这样,就对系统的性能和可用性提出了极大的挑战,带来实现的 复杂度。 此外,在特殊的情况下,仍然会可能带来数据的不一致,比如 value 和时间相关,或者是随 机数,两个 Redis 服务器所在系统的不一致带来了数据的不一致。 考虑到以上情况,为了解决复制问题,我们决定采用伪 slave 的方案,即实现 Redis 协议, 伪装成为 Redis slave,让 Redis master 推送数据至伪 slave。这个伪 slave,我们把它称为 keeper,如下图所示: 架构篇 8 有了 keeper 之后,多数据中心之间的数据传输,可以通过 keeper 进行。keeper 将 Redis 日 志数据缓存到磁盘,这样,可以缓存大量的日志数据(Redis 将数据缓存到内存 ring buffer, 容量有限),当数据中心之间的网络出现较长时间异常时仍然可以续传日志数据。 Redis 协议不可更改,而 keeper 之间的数据传输协议却可以自定义。这样就可以进行压缩, 以提升系统性能,节约传输成本;多个机房之间的数据传输往往需要通过公网进行,这样数 据的安全性变得极为重要,keeper 之间的数据传输也可以加密,提升安全性。 二、高可用 任何系统都可能会挂掉,如果 keeper 挂掉,多数据中心之间的数据传输可能会中断,为了 解决这个问题,需要保证 keeper 的高可用。我们的方案中,keeper 有主备两个节点,备节 点实时从主节点复制数据,当主节点挂掉后,备节点会被提升为主节点,代替主节点进行服 务。 提升的操作需要通过第三方节点进行,我们把它称之为 MetaServer,主要负责 keeper 状态 的转化以及机房内部元信息的存储。同时 MetaServer 也要做到高可用:每个 MetaServer 负 责特定的 Redis 集群,当有 MetaServer 节点挂掉时,其负责的 Redis 集群将由其它节点接 替;如果整个集群中有新的节点接入,则会自动进行一次负载均衡,将部分集群移交到此新 节点。 Redis 也可能会挂,Redis 本身提供哨兵(Sentinel)机制保证集群的高可用。但是在 Redis4.0 版 本之前,提升新的 master 后,其它节点连到此节点后都会进行全量同步,全量同步时,slave 会处于不可用状态;master 将会导出 rdb,降低 master 的可用性;同时由于集群中有大量 数据(RDB)传输,将会导致整体系统的不稳定。 截止当前文章书写之时,4.0 仍然没有发布 release 版本,而且携程内部使用的 Redis 版本为 2.8.19,如果升到 4.0,版本跨度太大,基于此,我们在 Redis3.0.7 的版本基础上进行优化, 实 现 了 psync2.0 协议,实现了增量同步。下面是 Redis 作 者 对 协 议 的 介 绍 : https://gist.github.com/antirez/ae068f95c0d084891305。 架构篇 9 三、DR 切换 DR 切换分为两种可能,一种是机房真的挂了或者出异常,需要进行切换,另外一种是机房 仍然健康,但是由于演练、业务要求等原因仍然需要切换到另外的机房。XPipe 处理机房切 换的流程如下:  检查是否可以进行 DR 切换 类似于 2PC 协议,首先进行 prepare,保证流程能顺利进行。  原主机房 master 禁止写入 此步骤,保证在迁移的过程中,只有一个 master,解决在迁移过程中可能存在的数据丢 失情况。  提升新主机房 master  其它机房向新主机房同步 当然了,即使做了检查,也无法绝对保证整个迁移过程肯定能够成功,为此,我们提供回滚 和重试功能。回滚功能可以回滚到初始的状态,重试功能可以在 DBA 人工介入的前提下, 修复异常条件,继续进行切换。 根据以上分析,XPipe 系统的整体架构如下所示: Console 用来管理多机房的元信息数据,同时提供用户界面,供用户进行配置和 DR 切换等 操作。Keeper 负责缓存 Redis 操作日志,并对跨机房传输进行压缩、加密等处理。Meta Server 管理单机房内的所有 keeper 状态,并对异常状态进行纠正。 架构篇 10 四、测试数据 我们关注的重点在于增加 keeper 后,平均延时的增加。测试方式如下图所示。从 client 发 送数据至 master,并且 slave 通过 keyspacenotification 的方式通知到 client,整个测试延时 时间为 t1+t2+t3。 首先我们测试 Redis master 直接复制到 slave 的延时,为 0.2ms。然后在 master 和 slave 之 间增加一层 keeper,整体延时增加 0.1ms,到 0.3ms。相较于多个 DC 之间几毫秒,几十毫 秒的延时,增加一层 keeper 带来的延时是完全没问题的。 在携程生产环境进行了测试,生产环境两个机房之间的 ping TTL 约为 0.61ms,经过跨数据 中心的两层 keeper 后,测试得到的平均延时约为 0.8ms,延时 99.9 线为 2ms。 综上所述:XPipe 主要解决 Redis 多数据中心数据同步以及 DR 切换问题,同时,由于 XPipe 增强后的 Redis 版本优化了 psync 协议,会极大的提升 Redis 集群的稳定性。 同时,整个系统已经开源,欢迎大家一起参与优化整个系统: XPipe: https://github.com/ctripcorp/x-pipe XRedis(在 Redis3.0.7 版本上进行增强的版本): https://github.com/ctripcorp/redis 架构篇 11 X-Series 企业级开发实践 [作者简介]赫杰辉,携程框架研发部高级研发经理,负责携程 DAL 组件开发与推广。在开发 一线奋战多年的老兵,热爱中国传统文化和推广开源软件,希望用自己开发的工具为大家解 决实际问题,愿为中国的开源事业贡献自己的绵薄之力。 前言 X-Series 是我开发的可视化系统快速开发框架。它的功能是为系统的处理过程,状态变迁和 复杂逻辑判断提供一个可视化的,基于流程图,状态机和决策树的建模平台和运行平台。该 框架是我基于多年的一线开发经验,针对实践中难以解决的痛点而研发的。使用 X-Series 开 发能够极大简化系统设计,降低开发难度,提高易用性。 好东西应该大家分享,我希望向广大开发者推广这套框架。但由于 X-Series 之前只有我自己 在项目里使用过,因此能不能够让大多数人接受还未可知。2016 年初某医疗领域初创互联 网公司需要做网站建设。公司开发负责人希望我可以帮助做系统的架构设计和技术选型。我 向其推荐了 X-Series 作为快速开发框架。 该项目开发正式启动后我帮他们用 X-Series 中用于处理流程构建的 XrossUnit(简称 xUnit 或 Xunit)设计好顶层流程和一个子流程作为培训示例,指导他们熟悉工具的使用。 在一个月的时间里,他们已经熟练掌握了 xUnit 并成功完成第一版可用系统并上线试运行。 从那时起到现在接近一年的时间里,开发团队一直使用 Xunit 构建系统,同时还自己尝试并 成过使用了 X-Series 中的 XrossState 管理主要业务模型的状态处理。通过和他们交流使用 体验,可以说 X-Series 在该公司的使用是成功的。 X-Series 使用效果说明 为了说明使用 X-Series 确实能够使系统在不断的演变中始终能够保持易于理解,易于维护。 我将展示基于 xUnit 的系统最初模型和该系统最新的模型。通过这种直观的对比我们可以看 到使用 xUnit 可以让系统始终保持清晰易懂的状态。  最初系统模型 系统主流程 架构篇 12 子流程示例 架构篇 13  最新系统模型 系统主流程 子流程示例 架构篇 14 架构篇 15 通过上面的介绍可以看到流程图可以有效的描述和分解系统。我相信任何一个有多年开发经 验的人都一定会觉这就是渴望已久的工具。 架构篇 16 用户反馈 下面是该公司主力开发人员给我的使用反馈:  学习安装 Xunit 的 demo 和介绍在相关的 GitHub 已有介绍。引入 Xunit 只需 pom.xm 中引入相关 dependency。  使用感受 配置文件的编辑,GitHub 也有介绍,确实清晰方便,三视图配合,一目了然。  承担角色 如果将系统看一个人,那么可用 Xunit 来搭建人的骨架。 以某服务系统为例,从服务总入口,到服务的分发,再到每个服务的业务逻辑切分都配置在 Xunit。整个系统的服务功能清晰明了,能快速定位到每个功能点,极大方便了后续的维护。 而每个服务的业务逻辑也是一目了然,方便定位业务的节点,调整业务的逻辑。 业务逻辑切分示例 架构篇 17  吐槽 Xunit 的图形化编辑器已经满足开发所需,但对于懒人来说,总是会追求更懒的方式,所以 这里还是要吐槽一下编辑器。首先是如上图所示,不能放大缩小;其次是 Xunit 不能切分成 多个小的 Xunit,需要自己写 Processor 去额外处理;每个元件不能通过复制粘贴来复用;没 有搜索功能。 以上是用户的反馈,这些功能都会想办法在未来的版本中提供。 总结 流程,决策,状态这些抽象模型往往是系统设计中最重要,最复杂,也是最难处理好的部分。 使用 X-Series 能够将这些模型从代码中抽离出来,以可视化的方式为这些抽象概念提供一 个创建,修改,交流和高效运行的平台。通过这个平台可以很快完成从整体到部分的设计和 分解,从而使总的设计工作量大大减轻。 通过 X-Series 完成总体设计,开发人员可以仅考虑很小范围内的需求,即有助于做出高内 聚,低耦合的设计,也有助于降低测试难度,提高测试效率。X-Series 可使抽象的系统设计 原则通过工具来具体化和可操作化,无需生搬硬套。在标准化的同时,也降低了对人员能力 的要求。原生的模块化允许即使设计有误,也可方便迅速的做到故障定位,隔离并替换。 虽然 X-Series 的三个组件非常直观,基本上都属于可视化的 DSL,但在实际的推广分享中, 不同的开发人员理解接受程度还是有一定的区别。XrossUnit 基于流程图,使用起来没什么 门槛;XrossDecison 需要了解什么是决策树,对于没有接触过类似概念的开发人员来说稍微 有点难度。XrossState 涉及到对状态机的运用,最好具备一定的背景知识。 作为一个开源的,通用的解决方案,使用 X-Series 在达到提高开发效率,质量的同时,还能 将充满困难,风险,不确定因素的开发过程转变为简单的,标准的,可重复的过程。不但在 当前的工作中可以有效解决问题,其积累的经验也有助于在未来的工作中有效的复用。如果 对使用方面有任何疑问,可以加入下方的技术支持组。 架构篇 18 参考 项目地址: https://github.com/hejiehui/xross-tools-installer X-Series 技术分享视频: https://v.qq.com/x/page/c0340vrpod1.html X-Series 设计思路详细介绍: http://blog.csdn.net/ctrip_tech/article/details/53337622 架构篇 19 携程高可用架构的演变和迭代——应用开发者视角 [作者简介]周源,携程技术中心基础业务研发部高级研发经理,从事软件开发 10 余年。2012 年加入携程,先后参与支付、营销、客服、用户中心的设计和研发。 前言 携程的架构经历了长期的演变和迭代,其中多个产品已经历了 5 次以上更新换代。每次迭代 都有其背景和出发点,都解决了前一个版本的痛点又不可避免地带来一些新的问题或遗漏一 些问题。这种迭代过去、现在,以及将来将一直持续,其中经历可圈可点,值得技术人细细 品味。 本文会首先从总体介绍携程架构的组成,然后以发布系统、配置管理和 SOA 三个实际案例, 详细介绍架构迭代,最后以 UserProfile 项目具体介绍携程架构亮点的点滴。 声明一下,本人现担任携程用户帐户信息的开发负责人,文章更多是从一位基层团队负责人 和一线开发人员的角度给大家分享携程架构历程。 文中涉及架构的方方面面,其中运维相关内容由运维团队负责;架构相关内容由架构、框架、 工具各团队负责;应用相关内容除用户帐户信息以外都是由其他开发团队负责。对于不是本 人负责的产品,文章仅站在使用方、合作方的角度,作客观、公正、事实的描述,且已尽量 争取各团队负责人的授权、收集各团队负责人的建议。 一.架构的组成 总体来说,架构由三部分组成:运维、框架、应用。 架构篇 20 1.1.运维 谈到高可用和稳定性,我们首先想到的肯定是运维。携程的运维是应用和架构坚强的后盾, 主要有四大亮点。 1.1.1.集群管理策略 携程的 Web 集群有 slb 控制流量,根据 healcheck 的结果可以自动拉出和拉入。发布和扩容 过程对开发透明,当机器 check 成功且没有报错时,机器将拉入集群。当 check 失败或单位 时间报错超过阀值,机器将自动拉出集群。 1.1.2.FullDR 机制 Web、DB、Redis 集群都有长效的 FullDR 机制,当一个 IDC 完全挂掉,比如网络故障、网线 拔断等发生时,FullDR 将发挥功效。携程定期对 FullDR 进行演练,以确定 DR 对订单的影 响。 1.1.3.DBA 策略 数据的安全是重中之重,携程将用户数据放在稳定的首位。我们使用 M-S 机制和 FullDR 结 合保证数据的高可用。同时为了因应互联网的发展,我们将 MSSQL 的数据无缝迁移至 MYSQL,虽然花费了很多时间和成本,但是为了稳定,投入也是值得的。同时我们保证迁移 过程对用户是透明的。 SQL+NoSQL 的结合是互联网发展的趋势,而携程的数据存储更是包含 MSSQL、MYSQL、 Redis、Hive、ES 等多种方式和技术,保证数据的高可用、最终一致性。 1.1.4.NOC 机制 在携程,作为开发负责人是非常艰苦的,因为如果你负责的应用一旦出现异常,NOC 7*24 小时都可能联系你。NOC 通过专门的订单大图和异常图表监控所有应用的运行状态。订单 量同比、环比的上升、下降都会被严密的监控。 1.2.框架 架构篇 21 框架是应用的基石,而携程框架更是经历过且正在经历着演变和迭代。其中特别值得分享的 包括: 1.2.1.SOA&Gateway SOA&Gateway 是服务的治理平台。有着非常悠久的历史,我后面会详细展开。 1.2.2.发布系统 携程的发布系统集成了很多特色功能,比如刹车、回退、版本切换、共用 dll 打包、pom 检 测等等。发布系统经历了历史上最严重的灾难性故障,在故障中浴火重生,非常值得给大家 分享其演变和迭代。 1.2.3.消息队列 市面上开源的消息队列工具非常多,Storm、MSMQ、ActiveMQ、RabbitMQ 等。携程结合 各第三方的优点,加以融合,结合自身情况,自主研发了消息队列。核心功能有 Partition 有 序、异步补偿和消息生命周期跟踪。 1.2.4.配置管理 配置管理在任何规模的公司都会做,而对配置而言最重要的不外乎是便捷、高效和高性能。 携程配置管理的演变恰恰反映了这种趋势。 1.3.应用 架构篇 22 经过和多家知名互联网企业架构师沟通,看下来大家的应用架构都是比较相近的,一般都会 用到 PreLoading&LayerLoading、Sharding、熔断、限流、降级等技术。 而经过无数经验证明,上述措施确实极大的提升了网站和 APP 的稳定性。比如,当灾难发 生时,PreLoading 可以保证用户可以看到预设的内容;而网络情况较差情况下,LayerLoading 可以保证用户操作不卡顿。 2.架构的演变 2.1.发布系统 携程发布系统至今大体经历了四个“年代”。 1) ITSM 2) CITSM 3) CRoller(ROP) 架构篇 23 4) Tars(CD) 说到发布,一定要提一下 “最传统”的发布方式。传统公司会有专门的售后团队负责部署、 或直接由开发人员负责发布。发布方式简单粗暴,直接登录到服务器上覆盖文件。 携程作为互联网企业,第一代发布系统已经做到了开发和发布隔离,使用一个 C/S 的软件名 叫 ITSM 做发布,发布人员只需要简单点击按钮就可以完成发布。但是那个年代,一旦提到 发布,我们往往就先要买第二天的早饭了。因为一个集群上的若干应用发布是排队的,必须 一个应用发布且验证完毕才发第二个。同时因为是 C/S 结构,需要发布人员做本地安装,使 得协同工作特别困难。 鉴于 ITSM 不断被诟病,携程自主开发了 CITSM 发布系统,功能和 ITSM 相似,但用 B/S 实 现,协同发布变成可能,且将发布系统与框架其他系统进行整合,为开发人员提供了极大的 便利。同时引入版本管理和回退机制,形成了一个飞跃。 第三代的发布系统进一步收紧了开发人员的权限,引入了 All In One、ConfigGen、自动加载 等。所谓 All In One,是将原本配置在 database.config 中的内容,由发布系统实现,开发不 再需要知道 DB 的连接字符串信息,取而代之的是获得一个 Key,在代码中配置这个 Key, 由发布系统在发布过程中将这个 Key 翻译成 DB 连接字符串。但第三代发布系统因为集成功 能太多,自身权限过大,最终导致了一个重大的生产故障,该故障以后第三代发布系统连人 带系统都被淘汰了。 取而代之的是第四代发布系统,被取名叫 Tars(又名 CD)。针对前三代发布系统最致命的漏 洞:发布都是本地备份。Tars 引入了异地备份,即使本地磁盘整个被清空,仍可以从远程恢 复。网站的稳定性又得到了质的飞跃。 2.2.配置管理 其次值得一提的就是配置管理。携程的配置管理大体也经历了 4 个时代。 架构篇 24 第一代配置系统,将 web.config 做了简单的封装,提供 Web 页供开发人员做编辑,故有简 单便捷等优点。对开发人员非常友好。 第二代配置系统恰相反,将 config 的修改集成在发布中,直接导致 config 等于一个全局变 量。这样避免了网站的重启,对用户很友好。但开发也就不用 config 了。 第三代配置系统是颠覆性的,一改传统 config 的缺陷,改为在应用启动时通过服务获取配 置信息,加载到内存中。当配置发生变化时,触发监听机制更新。但第三代配置系统仅支持 开和关两个状态。 第四代配置系统支持 Json 等主流格式,且优化了监听机制,并做了开源。 2.3.SOA SOA 在携程一直有着特殊的地位,在历史上也有更多有趣的故事。其演变和迭代过程值得 我们细细品味。 传统的 API 调用,是一种网状结构,难以管理和控制。故障的排查也异常的困难。如果处理 不当可能出现循环调用的情况,当服务端地址变化对客户端将是一场灾难。 携程作为互联网企业,吸取上述教训,在第一代 SOA 就引入了治理平台,统一管理服务的 地址。推出一个称为 ESB 总线的服务,所有调用方都请求 ESB,由 ESB 负责寻址和分发。此 种架构开始十分优美和清晰,但却有个致命的问题,ESB 总线是那个最大的瓶颈。那个年代, 90%的故障来自于 ESB 总线。 第二代 SOA 主要就是为了解决第一代 SOA 瓶颈问题,改为服务直连。SOA 仅作为治理和注 册,在调用方应用启动时从治理平台获取服务端的 URL,并存到内存中。之后调用方就可以 架构篇 25 直接调用。第二代 SOA 的口号是“直连和去 ESB”。 随着时间的推移,公司逐渐意识到在 SOA 层面可以做更多,比如熔断、限流、动态路由等。 熔断即治理平台会根据服务提供方的异常情况,决定是否回应调用方的请求,如果服务提供 方异常,有返回默认值、返回空值、直接报错几种可能。限流则重点监控服务提供方的连接 数,如果超过阀值,则开启队列模式,阻止之后的请求。第三代 SOA 集成了大量实用功能, 且做了大量监控、埋点,逐渐得到大家认可。 而进入无线时代后,H5 和 APP 和服务端的交互成为了业界研究热点,而 gateway 这次就呼 之欲出了。Gateway 取代了原先 MobileService 设计,加入了反爬和 Auth 认证。使得 SOA 的使用范围进一步提升。 3.UserProfile 架构篇 26 结合本人负责的“UserProfile”项目,给大家简述一下携程的架构亮点。 3.1.组成 “UserProfile”作为大数据的核心组成部分,由典型的大数据模型构成。包括注册、采集、 计算、存储、查询、监控六大功能。 其中采集的数据来源包括个人信息、常旅信息、联系人信息等用户信息、用户行为信息、用 户订单信息等。用户行为和用户订单采集的架构图如下所示。 3.2.架构 架构篇 27 采集到的信息通过 Batch 和 Steaming 两种通道,经过计算汇总到 UserProfile 仓库中。实时 通道采用 Kafka+Storm 以及携程自主研发的 Hermes 消息平台。 目前存储在”UserProfile”仓库中的数据已经达到 100 个亿条以上,而所有储存介质,包括 Hive 、MYSQL、Redis 都是用 FullDR + M-S 设计。如下图: 架构篇 28 在这样的数据量级下,服务平均响应时间一直控制在 10ms 左右(包括网络消耗 4ms)。使 用了熔断、限流、降级和 Sharding 组成了完整的架构保障,以实现整体的高可用。 架构篇 29 携程第四代架构探秘之运维基础架构升级 [作者简介]本文由携程技术中心框架研发部吴其敏、王兴朝,技术保障中心高峻、王潇俊、 陈劼联合撰写。 作为国内最大的 OTA 公司,携程为数以亿计的海内外用户提供优质的旅游产品及服务。2014 年底携程技术中心的框架、系统和运维团队共同启动了架构改造项目,历时 2 年,涉及所有 业务线。本文回顾了携程在整个技术架构改造过程中的一些实践和收获。 一、写在前面 随着携程业务量迅速增长、业务变化越来越敏捷,对于应用交付的效率也提出了更高的要求。 根据统计,截止 2014 年底携程总应用数在 5000 个左右,平均每周约有 3000 次以上的发布 需求。所以作为整体交付环节中极为重要的一环,应用的部署和发布是提高交付效率的关键, 然而携程原来的发布系统 Croller 却成为了阻碍交付效率提升的一大瓶颈。 【关于携程火车发布】 *携程火车发布规定:每天定时安排发布车次,以 pool 为单位安排车厢,在一个 pool 中的 应用必须在“同一车次”的“同一个车厢”内做发布。 *携程实际发布情况:每个应用在发布前需要“买票”,也就是申请和备案的过程,然后被 分配到某个“车次”与同在一个 pool 且需要发布的其他应用形成一个“车厢”,当到达规 定发布时间时,该“车厢”内的所有应用以灰度的方式做发布。 *该模式的弊端:(1)如果提前准备好了发布,在未到达规定发车时间,只能等待,不能发 布。(2)如果错过了某个发车时间点,只能等待下一次。(3)如果发布过程中,同一个车厢 内有一个应用发布失败,则整个车厢中的应用全部发布失败。 具体来说,携程 Croller 设计的是火车模式发布,主要面临的核心问题包括: (1)由于 ASP.NET 的应用占大多数,基本都采用的是 Windows + IIS 的单机多应用的部署 模式,应用和应用之间的隔离性较弱,且由于应用划分的颗粒度比较细,在单机上往往可能 同时部署 20~30 个应用,多的甚至达到 60 个,导致大量不同应用之间共用应用程序池的情 况存在,即多个应用运行在同一个进程下,这种情况下任何一个应用的发布都可能影响到其 他的关联应用。 (2)使用硬件负载均衡设备承载应用的访问入口,以域名为单位隔离。单机上的多个应用 程序共享同一个访问入口(同一个域名),所以健康检测也只能实现到服务器级别,无法识 别应用级的故障。 (3)由于治理系统中的应用信息不统一或不准确,影响监控和排障。 二、从破题到解题 架构篇 30 1.破题思路 针对混乱又复杂的情况,如果要想从根本上去解决这些问题,提高交付效率,则必须要从配 置管理、部署架构上全面支持以应用为最小颗粒度的管理能力。 我们解决思路包括: (1)引入 Group 的概念,设计从 App、Server、Pool、Group、Route 的完整数据结构模型 来描述应用相关的配置部署信息,并由 CMS 作为权威数据源向外提供数据接口,确保数据 的一致性和准确性。 这些定义如下: (2)引入七层负载均衡(SLB),实现应用的访问入口的隔离。使每个访问入口(集群)的 成员(即应用进程实例)可具备独立的管理能力,实现应用级的健康检测。 架构篇 31 (3)设计实现新一代的发布系统 Tars,解决 Croller 发布系统的痛点,支持应用级的发布。 架构篇 32 2.具体实现 虽然有了破题思路,但具体实现仍然有很多细节需要考虑,主要包括: (1)统一配置(CMS) (2)弹性路由(SLB) (3)想发就发(TARS) 统一配置(CMS) 正如大型传统企业发展初期缺失 ERP 系统一样,互联网公司也需要发展到一定规模才能意 识到一个完备的配置信息系统的重要性。 互联网公司在整个产品研发和运行生命周期中不断产生大量的系统和工具,例如测试平台、 发布平台、监控系统和资源管理工具等。业务产品研发效率和业务系统稳定运行依赖这些工 具平台的高效协同工作。而如果要实现这种高效协同,关键就是拥有一个统一的配置信息平 台。 不成熟的配置管理往往有以下特征: (1)配置系统之间相对独立和分散,缺少关联关系,且运维、研发工具、测试生产环境都 有各自视角的局部配置管理系统; (2)缺少明确的定义和抽象。例如,不同语言开发的应用对配置的描述方式有很大的差异 性;或者对集群、发布节点和访问入口等重要对象的定义很模糊; (3)配置信息不准,依赖手工维护,没有工具和流程约束,要执行者自己来保证操作和配 置数据的一致性; (4)配置描述不完整,使得系统架构,比如集群、域名映射等关键环节缺乏严格的配置管 理。 而携程这种配置管理暴露出很多问题,当监控到服务器资源异常时,例如 CPU、内存出现异 常,无法快速查找该异常影响的范围,造成不知道应该联系谁进行排障;而对于非.NET 的应 用无法提供发布工具,或只是提供了一些功能有限的发布模块,但由于不同技术使用的发布 工具有着很大的差异性,给使用方和开发维护方都带来了极大的不便;当资源和应用之间的 关系不清晰,运维无法实现完善的资源计费等重要管理职能。 要应对这些问题,需要定义统一的配置模型和一致的配置数据,清晰地描述组织、业务、应 用、系统架构和资源等要素及互相间的关系。从更高层次设计一套配置管理系统,使得各个 维度的配置信息既要专注于自身的领域,又能和其他相关的配置信息维度建立起关联,确保 这些工具能以一致的定义来理解配置数据,进行顺畅而有效的协同工作。 架构篇 33 于是携程的配置管理系统 CMS 就应运而生了,CMS 核心目标包括: (1)数据准确(即与实际保持一致)且合规 (2)数据关系的查询方便高效 (3)数据变动可追溯 (4)系统高可用 (5)数据模型简洁易懂 1. CMS 系统演变过程 (1)抽象,定义,建立关系,存储数据; 对于应用层面运维所涉及到的对象进行统一地抽象,使得使用不同技术、不同架构的应用体 系都能使用一样的模型结构来进行描述。 根据携程的应用体系和管理方式,我们抽象出一套最核心的应用配置对象,包括组织、产品 线、产品、应用、集群、发布节点、服务器等。经过与那些不同语言不同技术架构所开发的 应用间的磨合实验,我们验证了这套抽象的配置对象有其普适性,并可以完备地描述携程范 围内各种应用的配置状态。 只要按照这套配置对象系统对一个应用完成了描述,那么该应用从发布到上线运行再到下线 的全生命周期内,各种相关工具均能通过获取这些配置状态得到足够的信息进行工作。换句 话说,通过这套统一的配置信息数据库,不同开发者、不同阶段、不同功能的平台实现了协 同工作。 (2)将 CMS 作为一种服务提供出去。 架构篇 34 由于建立了描述应用体系的核心配置数据库,这必然会有大量用户和工具成为 CMS 的消费 者。所以我们希望 CMS 消费者可以通过网络随时随地获取、维护和管理 CMS。这要求 CMS 能提供完备的 API 和一套简洁直观的管理界面。 (3)通过 Portal 和工作流引擎完成配置变更,实现业务逻辑的自动化执行。 除了建立统一的应用配置模型,还要建立应用配置的生命周期管理,做到生成配置,修改配 置以及销毁配置都合规,都经过授权,都有记录可查。 (4)搭建一个强壮可靠的配置管理体系。 通过更多的子模块助力搭建配置管理体系来提高稳定性和可用性,实现查错追溯和数据巡检 纠错等功能。 2. CMS 系统架构 CMS 系统在开发过程中遇到和解决了一系列的棘手问题,系统本身的架构也反映了这些方 案的设计实施情况。 (1)数据治理 CMS 系统最基本而关键的需求是提供正确的数据,数据必须能真实反映生产环境的配置现 状,并且还要符合公司制定的运维规范,不能出现违规配置。例如,携程不允许同一个应用 在一台服务器上运行多于一个实例;不允许在一台服务器上运行多于一个 Java 应用;每个 服务器上只能运行同样类型的应用等。 所以为保证数据的准确性,CMS 数据需要持续治理。我们把这部分的治理工作通过一个相 对独立的规则引擎来实现。该规则引擎主要完成的工作包括: 架构篇 35  允许快速添加新规则,可以使用轻量的脚本语言快速定义各种规则进行数据检查;  针对复杂规则设计了场景和规则两层结构,不同场景可根据需求来配置不同规则;  数据入库时做检查,并进行定期巡检,最大限度查找和消灭错误配置。 (2)关系管理和变更追溯 对配置数据关联关系的管理和使用是 CMS 用户最为看重的功能之一。被 CMS 管理着的组 织、产品、应用、集群、服务器、域名、发布节点等配置间都有着千丝万缕的复杂关系,用 户可能从任何一个配置对象开始查找与另一个配置对象的关系,比如从应用查找服务器;从 服务器查找组织;从域名查找应用等等。 为提供最便利强大的查找功能,我们专门设计了一套查询框架,根据定义好的对象关系快速 生成配置对象之间的查询。现在用户可以通过 CMS 界面和 API 非常方便地查找到配置数据 间的关联关系。 与此相关的还有变更历史的查找,用户除了需要查找一个配置对象自身的变更历史外,还经 常需要查找一个配置对象相关的对象变更历史,比如要查找一个应用下面所有服务器的扩容 缩容历史;查找一个集群中应用上下线的历史等等。于是我们采用了一种将变更消息沿对象 关系链广播出去的方案,把变更和相关配置对象连接起来。 架构篇 36 (3)完善的监控和应对访问压力 CMS 因汇聚了生产环境核心的配置数据而被大量工具所依赖,因此其必须能够承受大量而 密集的查询需求(工作时间内每分钟上万次请求是常态)。 下图是携程接口网关日志分析出的各种工具对 CMS 接口的调用情况。 弹性路由(SLB) 携程部署架构采用的是单机多应用,每台服务器上部署了很多个应用。这些应用不一定存在 紧密内联关系,且很可能属于不同团队,这种架构存在着明显的问题。 其实携程面临的这些问题并不是突然暴发的,而是经过十多年的演进和慢慢累积,最终大家 架构篇 37 不得不正视这些问题。 从本质上讲,这些问题的根源是应用间的耦合,最好的解决方案就是单机单应用。因为单机 单应用实现了应用间的天然物理隔离(部署在不同的服务器上),从而极大地降低了运维的 复杂度,部署、排障、沟通、配置和个性化等都不用再担心会对其他应用有影响。 单机单应用是业界普遍推荐和采用的一种部署架构,但对携程而言这却是个系统性的大工 程,需要从底层基础设施到配套系统工具、从流程规范到开发人员的思维转变等方面投入大 量的人力和时间。所以我们首先就要考虑如何在单机多应用的情况下,实现应用解耦,也就 是做到应用粒度的运维。 相比应用粒度的运维目标,携程当时实际情况则是服务器的运维粒度,并且绝大多数的运维 操作还是通过硬件 LB 来完成。虽然硬件 LB 的好处显而易见,例如,高吞吐量、高性能和优 秀的稳定性等。但其缺点也同样明显: (1)水平扩展成本高昂; (2)基于规则无法建模,规则过多时就会陷入运维泥潭; (3)无法进行高频次的变更,因为集中式管理模式中,配置数据一多,API 性能就会急剧下 降; (4)只能由少数的专职运维人员做操作。 所以,硬件 LB 除了无法做到应用粒度外,低效也成为一个很重大缺陷。为了解决在路由运 维方面的粒度和效率问题,携程决定打造自己的软负载(SLB)系统,替代掉硬件 LB 的七层 路由职责。经过讨论,SLB 确定了自己的职能目标,即可以高并发、实时、灵活、细粒度调 整七层路由规则。从另一方面想,SLB 还需要实现由面向机器运维到面向应用运维的转变, 以及由硬件支撑到软件支撑的进化。 在携程 SLB 的开发过程中,最重要的几点是: (1)面向应用建模; (2)多次更新一次生效 (3)多并发操作的挑战; (4)多角色运维冲突的问题; (5)监控和告警。 1. 面向应用建模 架构篇 38 携程经过评估最终选择了 Nginx 来构建软负载系统。开发前我们参考了业界内其他公司的 实现方式,基本包含几个特点: (1)开发了一个 Nginx 配置文件的批量管理工具; (2)需要专业的运维人员来操作; (3)日常操作频率较低; (4)和现有系统接合较松散。 结合携程的现状,我们在建模时还需要考虑: (1)和现有系统无缝接合,融入现有系统的生态体系; (2)支持高频率的并发操作; 但如何和现有建模体系融合起来?在开发人员眼中最重要最核心的常见模型就是一个一个 的应用。所以 SLB 要做的是如何和应用模型融合起来,换句话说,所有对 SLB 的操作都要被 抽象为对一个应用的操作。Nginx 是基于文本配置文件,其内建了一个自己的模型,一次运 维操作可以导致多个 Nginx 模型的变更。所以我们需要创建一个模型,这个模型可以和应用 模型一一对应,又能被翻译成 Nginx 的内建模型,而这就是 Group: (1)一个 Group 是一个应用在 SLB 的投影; (2)SLB 上所有的操作都抽象成对 Group 的操作; (3)不同 Group 的操作互不影响。 架构篇 39 这样只要解决一个 Group 的问题,就相当于解决了 1000 个、甚至更多个 Group 的问题。 2. 多次更新一次生效 建模成功地隐藏了 Nginx 的内存模型,并将操作转换成了对 Group 的操作。虽然隔离不同 Group 间的操作,但在 SLB 上对单一 Group 的操作仍然是一个有风险的行为(对某一具体 应用而言)。为了降低这种风险性,可以引入 3 种机制,包括多版本系统、日志追踪和多次 更新一次生效。 Group 的每次变更都会产生一个新的版本;Group 的所有变更都会留下日志;对 Group 的 变更操作并不会直接对生产生效,可以在多次变更后,有一次明确的激活操作后,从而在生 产环境正式生效。 架构篇 40 3. 多并发操作 引入 group 后实现了应用的独立运维,但如果有上千个 Group 要同时进行扩容操作,那么 如何做到每个 Group 的操作都在 5 秒内完成? 因为 Nginx 是基于一个文本配置文件的,那么这样的要求就会转换为对配置文件的上千次 操作,然后再对 SLB 重新加载上千次配置文件。假设一次操作花费 1s,那么最后一个操作 可能要等 1000s,这种实现方式显然对于那些排在后面的 Group 更新者是无法接受的,而且 SLB 在这种高频度更新下,自身也无法工作。所以简单地把一次 Group 更新转换成一次 Nginx 的配置更新是肯定行不通的。(携程真实情况是 Nginx 变更日操作达到 8 万次,整个软负载 API 日请求数达到 300 万次)。 为了实现 Group 更新的互不影响,并确保所有 Group 更新保持在一个稳定返回时间内,SLB 确定了核心业务流程: (1)将一段时间内所有的 Group 更新操作(比如 2 秒内)缓存在一个任务队列中; (2)对任务队列中的所有操作进行合并,最终只对 Nginx 的配置文件做一次更新。 架构篇 41 这个流程的核心逻辑就是多次操作一次更新,最大程度减少对 Nginx 配置文件的操作,但外 部看来 Group 更新操作是独立且保持在稳定返回时间内的。 4. 多角色运维的冲突 一个 Group 可能会有多种角色进行更新,比如应用 Owner、专业运维人员和发布系统人员 等。这就引出了一个新的问题,即当一个角色对一个 Group 的服务器进行拉出操作后,另 一个角色可不可以对这些服务器做拉入操作? 比如,发布系统人员在发布完成后,准备做拉入,却发现运维人员对这台服务器进行了拉出 操作。这时发系统应该如何决策?这不仅造成决策的困扰,也会使不同的角色产生联系,甚 至相互耦合在一起。 为了解决这个问题,我们决定采用多状态的机制: (1)为每一种角色分配一个服务器状态; (2)一个角色对这个状态进行了失效操作,最终也只能由这个角色进行恢复操作; (3)SLB 在所有角色都认为这台服务器有效时,才会认为这台服务器可工作。 架构篇 42 5. 健康检测带来的瓶颈 SLB 另一个核心功能是健康检测,即需要以一定频率对应用服务器进行心跳检测,连续失败 多次后对服务器进行拉出操作,成功后再进行拉入恢复。大多数公司采用了节点独立检测造 成了带宽浪费和服务器压力,而携程采用了节点共享检测,具体机制是一个独立的应用负责 检测,然后把检测结果在 SLB 节点间传播共享。 【携程的健康检测效果】 携程独立健康检测的运行效果良好,目前 SLB 系统已经负责了携程超过 5 万个结点的健康 检测任务。而下图是由节点独立检测变为节点共享检测时的 SLB 单一服务器网络连接释放 状况: 6. 监控数据采集和告警 SLB 负责了几乎所有的基于域名的 http 调度请求,所以也成为了进行请求流量统计和请求 质量统计的绝佳场所。包括在有问题时进行报警;根据不同维度统计请求量;响应码分布和 响应时间分布等,携程使用了分析 access log 的方式来获得监控数据: (1)SLB 服务器流式读取本机实时产生的 access log; 架构篇 43 (2)分析聚合 log 数据,产生不同的统计数据。最终使用了语法树分析实现了高效分析, 一秒可以分析 14 万条日志; (3)定期(1 分钟)将统计数据吐到监控系统 CAT 等。 以此可以产生多维度的监控统计数据,如下图: 架构篇 44 基于上述数据,可以查看整个携程或单个应用性能表现,进行相应的优化。在慢请求和非 200 请求的数量异常时,执行报警操作,确保及时恢复和挽回损失。 想发就发(TARS) 架构篇 45 解决了配置和路由问题后,发布系统前置障碍已基本扫除,而从 OPS 角度来看,发布系统 还有几个重要目标: (1)灰度发布 (2)简单易用 (3)发布迅捷 1、灰度发布 通常发布有三种常规方法,蓝绿发布,滚动发布,金丝雀发布。对这三种发布类别做比较, 可以发现: (1)蓝绿发布:需要额外的服务器集群支持,且数量可观,同时由于携程单机多应用的部 署现状,就会造成发布一个应用需要替换整台服务器的情况,实现难度巨大且成本不经济。 (2)滚动发布:虽然可以节省资源,但对应用的兼容性有较高要求,因为发布过程中同时 会有两个版本对外提供服务。但这类问题相对容易解决,实际中往往会通过功能开关,dark launch 等方式来解决。 (3)金丝雀发布,比较符合携程对灰度发布的预期,但可能需要精细的流控和数据的支持, 同样有版本兼容的需求。 【发布相关说明】 蓝绿发布:优先将新版本发布到待发布的机器上,并进行验证,此时新版本服务器并不接入 外部流量。发布和验证过程中老版本所在的服务器仍照常服务,验证通过后,经过流控处理 把流量引导到新服务器,待全部流量切换完成,老版本服务器下线。 架构篇 46 滚动发布:从老版本服务器中挑选一批,停止老版本的服务,并更新为新版本,进行验证, 通过后再分批增量更新剩余服务器。 金丝雀发布:往往从集群中挑选特定服务器或一小批符合要求的特征用户,对其进行版本更 新及验证,随后逐步更新剩余服务器。 结合携程的实际情况,最终挑选的方式是滚动发布和金丝雀发布的结合体,首先允许对一个 较大的应用集群,特别是跨 IDC 的应用集群按自定义的规则进行切分,形成较固定的发布单 元。每个应用的每个发布单元称为“group”,这个 group 与之前提到的 SLB 的 group 是一 一对应的。 每个发布单元,即 group 在发布过程时,还可以再分批进行,完成滚动发布。而每个 group 中包含一台或多台堡垒机,必须先完成堡垒机的发布和验证,才能继续其他机器的发布,从 而实现金丝雀发布。除堡垒机的发布外,其他机器可按照用户能接受的最大同时拉出比例来 分批,分批间允许设置具体的验证等待时间。 每台机器在发布过程中都要经历拉出、下载、安装、点火和拉入这 5 个步骤,发布流程为: 架构篇 47 基于以上设计,携程新一代发布系统开发完成,命名为 Tars 。 【Tars 源代码】 Tars 已做了开源,开源版本地址:https://github.com/ctripcorp/tars 2、简单易用 发布配置必须简单易懂,绝大部分的应用发布都是固定模式,不需要个性化配置,所以 Tars 只提供了几个核心配置项,包括(1)允许同时拉出的最大比例;(2)批次间的等待时间; (3)启动超时时间;(4)是否忽略点火。 除此以外,用户最关心的是发布过程中可操作按钮的易用性,Tars 在这方面做了充分考虑, 通过状态机的控制,保证用户在操作界面上同时最多只看到两个操作按钮,绝大部分情况下 用户只需在“继续”或“终止”这样的 0 或 1 的选择中做出决策。 而图形化界面的展示,Tars 也确保用户可以更直观地观察到发布的进展,以及出现的问题。 有了简单操作,危机时刻就会得到放大体现,比如,因生产故障做回滚时,能快速中断当前 发布,并从界面中轻松地选到所需回滚的版本,然后一键无配置地触发完成回滚。 架构篇 48 3、发布迅捷 天下武功无坚不摧,唯快不破,而发布也一样。发布速度快了,迭代速度研发效率也就提升 了;回滚速度快了,生产故障造成的影响也就减轻了;扩容速度快了,弹性计算就能实施了, 这样运维效率被大幅度提升。 从上面对发布过程的描述中,不能发现在携程通常影响发布速度的步骤是下载和验证。 (1)为了提高下载速度,携程在各个机房搭建了发布包专用的存储系统,实现了类似 CDN 的功能,编译和打包系统在任何一个写入点写入发布包,都会尽快同步到各个 IDC 及各个独 立存储中,这样真正发布时,服务器只需从本 IDC 或本网段做下载。而回滚方面,Tars 则是 在服务器本地保留了 n 个版本(n 根据服务器磁盘容量计算获得),做回滚时可快速地进行 目录切换,进而省略了代码下载过程。 (2)对于验证,携程在框架层面统一提供了验证入口和常规验证方法(携程称为“点火”), 收口了所有应用的验证规范和标准,容错性得到提升。 (3)Tars 在系统设计方面充分考虑了速度需求。每个发布单元采用 quick and dirty 的方式, 不管成功或失败,优先尝试把版本发布完成,后续在解决个别发布失败的问题。 根据同时拉出服务的最高比率(由用户设置)进行失败率控制,一旦达到比率,立即中断当 前发布,从而对 quick and dirty 方式做保护(携程称为“刹车”)。发布单元中只要有任何 一台服务器发布失败,都会被认为是发布局部失败,允许用户重试发布。 发布过程中如发现服务器当前运行版本与发布目标版本一致,且验证通过,则直接 skip。批 次间可设置观察等待时长,从第 3 个批次起,允许设置 0 或较少的等待时长,以提高后几批 次的速度(携程称为“尾单加速”)。 架构篇 49 三、结果和未来 通过 CMS+SLB+TARS 几个系统的联动,并经历了长达一年半的项目推广阶段,终于实现了 1+1+1>>3 的效果。新发布系统对于研发效率和研发人员体验的提升都非常显著。 这可以通过一些数字来证明,与 2 年前相比,每周的发布迭代次数成长了 4 倍,但单次发布 的平均时长从 13 分钟却降低到了 3 分钟。同时因为发布/回退效率的提升,当需要对线上代 码做紧急修复时,或者将其回退到已发布的代码版本时,都会更快捷地完成,所以使得发布 类故障的处理效率也得到了提升。 对 2015 年至 2017 年的发布相关故障的统计后,发现该占比下降了一半以上。 因为 CMS+SLB+TARS 基于良好的配置数据模型设计,及其应用级的运维支持能力,为后续 的技术架构改造带来了便捷和优势。这主要体现在: (1)高效的容量管理,实现了对应用容量的自动化监测,当发现容量不足时,无需研发介 入,全自动地进行应用服务器扩容、发布、上线和投产等。 (2)在应用容灾方面,基于准确的配置数据,可以很容易的将单数据中心的业务应用“克 隆”到另外的数据中心来进行部署。 (3)在应用技术栈的迁移(例如.net 应用改造为 java 应用),用户也能自助地创建新的 java 应用,并通过 SLB 灵活实现灰度流量切换,进而自助、高效、稳定、安全地完成整个应用迁 移。 架构篇 50 去哪儿消息消费模式 [作者简介] 余昭辉,2011 年加入去哪儿网技术团队。目前在平台事业部/基础架构部,参与 开发了可靠消息中间件,任务调度中心,订单中心等基础组件。个人对系统性能优化,利用 并行、异步构建高并发的系统很感兴趣,对编写 Clean Code 有执着的追求。 去哪儿网已经有四年多使用消息驱动架构风格构建大型交易系统的经验,现在整个交易链路 基本上都是靠消息来驱动完成,在这个过程中我们也不断地的摸索前进,积累了一些消息处 理的模式,而且我们还将这种模式以内置的方式提供出来,以期达到开箱即用。本文就根据 过去我们使用消息驱动积累的经验总结,并结合我们内部的消息队列 QMQ 作为讲解示例。 一、consumer 消费到重复消息怎么办? 1.1 消息消费模式 消息消费一般存在三种模式:最多一次,最少一次和有且仅有一次。 1.1.1 最多一次 这种可靠性最低,不管消费是否成功,投递一次就算完了。这种类型一般用在可靠性不高的 场景中,比如我们一个对日志分析展示的场景,如果这种日志分析出现一定的缺失对业务也 影响不大,那我们可以使用这种方式,这种方式性能最高(QMQ 的非可靠消息)。 1.1.2 最少一次 基本上所有追求可靠性的消息队列都会采用这种模式。因为网络是不可靠的,要在不可靠的 网络基础上构建可靠的业务,就必须使用重试来实现,那么重试就有可能引入重复的消息 (QMQ 的可靠消息)。 1.1.3 有且仅有一次 这是人们最期望的方式。也就是我如果真正的处理失败了(业务失败)你才给我重发,如果仅 仅是因为网络等原因导致的超时不能给我重发消息。但是这种仅仅靠消息队列自身是很难保 证的。不过借助一些其他手段,是能达到有且仅有一次的『效果』(QMQ 的幂等检查)。 通过上面的描述,我们知道有且仅有一次的消息投递模式是很难达到的,那如果我们需要消 息的可靠性,就必须接受重复消息这个事实。那么对于重复消息到底该怎么办呢?下面会列 出一些场景和解决方案: 1.2 处理方式 1.2.1 不处理 架构篇 51 这也算解决方案么?我当然不是说所有的重复消息都可以不处理的,但是是有场景是可以 的。比如我们有一个缓存,数据库更新之后我们发送一条消息去将缓存删掉,等下一次数据 访问的时候缓存没有命中,从数据库重新加载新数据并更新缓存。这里的消息就是删除缓存 的消息,那么删除缓存这个消息就是可以接受重复消息的,这种重复消息对业务几乎没有影 响(影响也是有,可能会稍微降低缓存命中率),我们权衡一下处理重复消息的成本和这个对 业务的影响,那么不处理就是个最佳方案了。可能有同学说,降低缓存命中率也不行啊,还 是得解决。那么我们看这个重复消息会降低多少命中率呢?那就得看重复消息多不多呢?重 复消息一般是网络不稳定导致的,这在内网里这种情况其实并不常见,所以我觉得是可以接 受的。 1.2.2 业务处理 有同学讲这不是废话么?重复消息当然是我们业务处理啊。我这里说的业务处理是说有很多 业务逻辑自身就是能处理重复消息的,也就是有很多业务逻辑本来就是幂等的。这里有个题 外话:即使我们不使用消息,也要尽量将我们的接口设计为幂等的。比如我们有一个创建新 订单的消息,接到消息后会向数据库保存新订单。那么如果我们接到了重复订单(订单号相 同),这样的订单肯定是不能保存的,但是这里切记一点,虽然最终我们不会保存两个一样 的订单,但是收到重复订单的时候你就回复成功就可以了,不要抛出异常,因为抛出异常一 般会认为是消息消费失败,又会重发。这在早期我们很多同学犯这个错误,就直接将 DuplicateKeyException 异常抛出了(其实对于接口幂等设计时也是一样,第二次重复调用的 时候你返回成功的响应就行了,如果要告诉人家是重复的也在另外的字段告诉,而不是标识 成功或失败响应的地方标识,这样会让请求方的处理代码更舒服些)。 1.2.3 去重表 如果我们不能接受重复消息,但是我们的业务逻辑自身又没办法处理重复消息该怎么办呢? 那就得借助额外的手段了。也就是引入一个去重表,我们在从消息里提取一个或多个字段作 为我们去重表的唯一索引,在消息处理成功的时候我们在去重表记录,如果又接到重复的消 息先查去重表,如果已经成功消费过这个消息则直接返回成功就行了。而且我们还可以根据 我们对去重这个事情要求的可靠等级选择将去重表建在不同的位置:数据库或 redis 等。放 在 redis 里那么去重的可靠性就是 redis 的可靠性,一般达到 99.9%应该是没有问题的,而且 放在 redis 里我们可以设置一个过期时间,因为重复消息这个东西一般会在一个短期时间区 间内发生,比如很少几个小时后甚至是几分钟后还出现重复消息。那么如果我们对可靠性要 求更高则可以将去重表放在数据库里,但使用数据库成本也更高,而且数据库一般没有自动 过期机制,所以可能还需要一个自动的『垃圾回收』处理机制,将多久之前的去重表里的数 据删除掉。看起来引入额外的去重机制是不是很麻烦?不用担心,QMQ 已经为你提供了幂 等检查器这种机制,只要简单的配置一下就 ok 了: 架构篇 52 上面三种方法基本上就可以解决绝大多数问题了。但是别离开,你真的以为去重就真的这么 简单么?并不是。我们再来仔细看一下,假设我们收到一个消息后我们处理变更一下数据库, 然后我们还要请求其他服务,如果现在的情况是我们的数据库更新成功了,但是服务请求失 败了,最后引起消息重发,这该怎么办?我们能再次调用这个服务么?并不确定。所以以后 如果有人给你提供服务,除了理解清楚这个服务的功能外,最重要的一点是这个服务是不是 幂等的,如果不是幂等的你应该要求服务提供方提供幂等的服务,这样你好他好大家都好(幂 等性是服务设计的重要原则之一)。 另外一点,如果我们的消费逻辑只涉及我自己的数据库操作,并不调用其他服务,但是因为 我自己的业务逻辑不能处理重复消息,所以我要借助去重表,但是我怎么保证去重表和我的 业务库操作是原子的呢?就是我的业务库操作成功了,去重表没有记录成功怎么办?这就要 引入事务了。放心,QMQ 已经为你考虑到了这一点提供了带事务的去重逻辑。关于 QMQ 的幂等检查请参照 QMQ 使用文档。 二、消息顺序 消息投递的顺序是消费者关心的第二个问题。很遗憾,实现顺序消费的成本也是非常高的, 所以大多数消息队列没有提供顺序消费模式。Kafka 因为它独特的存储模型,所以提供了顺 序消费这种方式,但是也是有其他限制的。那么如果我要求顺序消费该怎么办呢? 2.1 处理方式 2.1.1 不处理 这个我就不啰嗦了,其实这个场景可以直接借用上面重复消息里的场景,删除缓存的消息先 发的后到是没有多大关系的。 2.1.2 业务处理 和上面一样,绝大多数业务逻辑是本身就是能处理顺序的。比如我们的交易系统里有很多很 多状态机,状态机有严格的状态扭转流程。比如我们的支付状态机,我们从待支付->支付完 成->退款中->退款完成。那假设现在我们是待支付状态,然后用户支付后又立即申请退款, 那么有可能退款中的消息比支付完成的消息先到(这种几率也是非常非常低的),这是不可以 的,不满足状态机扭转条件,所以我们可以抛出异常告诉消息队列消费失败即可,等到后面 支付完成的消息到达后将状态扭转为支付完成,然后等到退款中的消息到之后才将状态扭转 为退款中。 2.1.3 额外字段 架构篇 53 2.1.3.1 不要求严格有序 那么如果我们的系统没有状态机这种东西,靠业务逻辑不好处理顺序该怎么办呢?那我们可 以借助额外的字段来处理了。一般在数据库设计中,我们都建议每个数据库表都有这样两个 字段:created_time 和 updated_time,也就是这条数据的创建时间戳和更新时间戳。那么如 果我们不要求消息严格有序的时候就可以借助 updated_time 字段来控制顺序了。比如我们 接到一条消息,然后需要更新数据库,然后我们发现消息中携带的时间戳比我们数据库中记 录的时间戳还小呢,那这条消息我不用消费了,直接返回成功就行了。使用这种方式有两个 限制:1. 不要求严格有序,只要有序就可以了,中间可能少几条的更新对业务没有影响。 2. 服务器之间的时间不能出现较大的偏差(这个通过时间同步服务一般都能保障)。 2.1.3.2 严格有序 那么如果我们要求严格有序呢?就是中间不能出现缺口。那么这种就不能依靠时间戳了,那 我们可以添加一个整型的 version 字段,消息里也携带了一个 version 字段,每次更新数据 的时候我们采用这种更新方式(乐观锁): update tbl set ......, version=version+1 where version=@message.version。如果这条语句没有更新成功,则返回行数就不为 1,这个时候 我们抛出异常让消息队列重试,等到合适的消息到来的时候就会更新成功了。使用 version 字段这种方式可能就要 producer 和 consumer 进行协调了(其实就是有些耦合了),因为消息 里也要携带 version 字段。但是设计这个 version 字段的时候也要一些考虑,我更建议的更 新方式是 update tbl set ......,version=@message.newversion where version=@message.oldversion。这样如果发送消息的 version 因为某些原因(比如有些更新并 不发送消息)没有严格递增两边也可以兼容。还有一点是,要控制 consumer 端数据的写入 点。比如我们的处理消息消费的地方更新数据外还有另外一个地方更新数据,这个地方将 version 给更新了,那么就会导致 producer 和 consumer 版本不一致,最后导致怎么也同步 不起来了。而 producer 方也要控制,比如如果我们人肉直接去表里修了数据可能就没有消 息发出了。 三、并发更新 既然说到 version,这里还提一下用 version 来处理并发更新吧。在去哪儿有很多场景里会将 数据库当做 key/value 存储使用,比如一个订单,我们的表设计可能是订单号,订单内容(一 个结构化 json),created_time, updated_time, version。我们不再使用列的方式来存储订单, 至于这种方式的优点和缺点就不在本文讨论之列了。那么如果来了一条订单更新消息,我们 需要对这个结构化 json 进行更新该怎么处理呢?会先读取这个 json,然后将消息里的内容 与 json 进行 merge 操作,然后将 merge 结果写回数据库。那如果我们在 merge 的过程中 订单已经被更新了怎么办,这就涉及并发更新控制的问题,如果不加以控制则可能导致更新 被覆盖。那我们一般采取的方式是: 架构篇 54 这种方式就可以避免并发更新冲突覆盖的问题了,但是冲突之后怎么办呢?一般会采取重试 的办法。我们会发现这种并发冲突的概率并不会很大,而且出现之后只需要重试一下基本上 就可以处理了: 我们发现这种处理方式在我们的场景中一遍又一遍的出现,那我们也将这种方式直接内置到 组件之中了(NeedRetryException): 四、异步处理 有的时候我们接到消息后并不是在接收消息的线程里处理消息,比如我们接到消息后会发起 一个异步的服务调用,那么我们并不能立即知道这个异步服务调用的返回结果。它的返回结 果是在另外的线程里,但是消息队列需要知道消息的消费结果。一般消息队列的 consumer client 会自动的返回消费结果,但是因为异步的方式切换了线程,所以需要应用显式的告诉 结果,这就需要使用显式 ack 的机制了: 但是我在与一些开发沟通过程中也发现了显式 ack 误用的情况: 1. 不管三七二十一,都自己开一个线程池,将消息丢到这个线程池里处理,然后使用显式 架构篇 55 ack。问原因,答曰开个线程池处理快些。实际上 QMQ 的消费者本来就是跑在一个线程池 里,而且这个线程池的参数是可以通过 QConfig 进行热配置的。如果你的消息没有异步处理 需求你就*不应该*自己开个线程池然后使用显式 ack 处理。 2. 使用显式 ack 一定一定一定要调用 ack。很多时候我们程序因为各种原因,比如中途抛出 异常,队列满了消息其实根本没有处理等等导致 ack 没有被调用。 五、批量处理 一般消息都是单条处理的,但是有很多时候批量处理是能提高性能的,比如数据库的批量插 入相比单条插入,ES 的批量更新相比单条更新等等。那么我们就需要将单条接收的消息转 成批量了。转成批量的方式一般就是先将消息入一个队列,然后在队列的那头用线程批量的 从队列里取消息,然后批量处理。不过批量处理也是有讲究的。 5.1 能批量就批量 这种方式是说如果我们的消费端本来就能处理好快啊,大部分时候都比生产要快,那我就没 有必要还批量了。比如虽然批量插入数据库的性能是比单条快很多,但是现在消息生产的 qps 就只有几十,我还要等一个个批量满就没有啥必要了,但是有的时候生产速度也会快一 些,那这个时候就可以批量了。这种方式能在性能和处理速度之间达到一个比较好的平衡。 恭喜你,QMQ 已经提供了这种处理模式了,在 QMQ Client 里提供了一个 BatchExecutor 的 类,就是专门为这种场景设计的。请看示例代码: 注意代码里的 ack 相关逻辑。BatchExecutor 的特性是如果批量部分处理的慢批量就大,但 是不会超过设置的最大批量大小,如果批量逻辑处理很快则可能就没有批量。 5.2 时间和批量二选一 上面介绍的 BatchExecutor 能在实时性和批量性能之间取得一个平衡,但平衡也不仅仅只有 架构篇 56 这一种方式。比如我们现在想通过TCP发送数据,我们知道TCP 传输数据的时候除了payload 以外还有一些 header 的,为了提高带宽利用率我们都期望让数据尽量填满每个 packet,但 是我们也不能为了填满 packet 就不顾实时性了,比如消息实在太小又太慢,可能好久才够 一个 packet,那我们也不能干耗着。所以我们就需要一个即有批量设置又有时间限制的批量 处理功能了。也就是批量和时间这两个条件谁先达到就先满足谁。还是要恭喜你,QMQ 也 为你准备好了这种功能了,请看示例代码: 5.3 多通道批量 批量的方式其实就前面两种了,但是有的时候我们批量操作的目标可能进行了划分。比如我 们进行了分库分表,那我们批量插入的时候就需要先对消息进行分组,然后不同的组批量进 入不同的表。那 QMQ 也已经准备好了这种模式: 架构篇 57 本文大致介绍了在消费消息时候会采用的一些模式,并结合去哪儿公司内部的消息队列加以 举例说明,希望有些帮助。 架构篇 58 Spring 探秘,妙用 BeanPostProcessor [作者简介]杨宗良,携程深圳机票研发部资深开发工程师,现参与国际机票 Online 项目的维 护及 JAVA 重构工作。 最近,在给项目组使用 Spring 搭建 Java 项目基础框架时,发现使用 Spring 提供的 BeanPostProcessor 可以很简单方便地解决很多看起来有点难解决的问题。本文将会通过一 个真实案例来阐述 BeanPostProcessor 的用法。 BeanPostProcessor 简介 BeanPostProcessor 是 Spring IOC 容器给我们提供的一个扩展接口。接口声明如下: 如上接口声明所示,BeanPostProcessor 接口有两个回调方法。当一个 BeanPostProcessor 的 实现类注册到 Spring IOC 容器后,对于该 Spring IOC 容器所创建的每个 bean 实例在初始 化方法(如 afterPropertiesSet 和任意已声明的 init 方法)调用前,将会调用 BeanPostProcessor 中的 postProcessBeforeInitialization 方法,而在 bean 实例初始化方法调用完成后,则会调 用 BeanPostProcessor 中的 postProcessAfterInitialization 方法,整个调用顺序可以简单示意 如下: --> Spring IOC 容器实例化 Bean --> 调用 BeanPostProcessor 的 postProcessBeforeInitialization 方法 --> 调用 bean 实例的初始化方法 --> 调用 BeanPostProcessor 的 postProcessAfterInitialization 方法 可以看到,Spring 容器通过 BeanPostProcessor 给了我们一个机会对 Spring 管理的 bean 进 行再加工。比如:我们可以修改 bean 的属性,可以给 bean 生成一个动态代理实例等等。 一些 Spring AOP 的底层处理也是通过实现 BeanPostProcessor 来执行代理包装逻辑的。 BeanPostProcessor 实战 了解了 BeanPostProcessor 的相关知识后,下面我们来通过项目中的一个具体例子来体验一 架构篇 59 下它的神奇功效吧。 先介绍一下我们的项目背景吧:我们项目中经常会涉及 AB 测试,这就会遇到同一套接口会 存在两种不同实现。实验版本与对照版本需要在运行时同时存在。下面用一些简单的类来做 一个示意: HelloService 有以下两个版本的实现: 做 AB 测试的话,在使用 BeanPostProcessor 封装前,我们的调用代码大概是像下面这样子 的: 架构篇 60 可以看到,这样的代码看起来十分不优雅,并且如果 AB 测试的功能点很多的话,那项目中 就会充斥着大量的这种重复性分支判断,看到代码就想死有木有!!!维护代码也将会是个噩 梦。比如某个功能点 AB 测试完毕,需要把全部功能切换到 V2 版本,V1 版本不再需要维护, 那么处理方式有两种:  把 A 版本代码留着不管:这将会导致到处都是垃圾代码从而造成代码臃肿难以维护  找到所有 V1 版本被调用的地方然后把相关分支删掉:这很容易在处理代码的时候删错 代码从而造成生产事故。 怎么解决这个问题呢,我们先看代码,后文再给出解释: 架构篇 61 现在我们可以停下来对比一下封装前后调用代码了,是不是感觉改造后的代码优雅很多呢? 那么这是怎么实现的呢,我们一起来揭开它的神秘面纱吧,请看代码: 架构篇 62 架构篇 63 我简要解释一下思路:  首先自定义了两个注解:RoutingInjected、RoutingSwitch,前者的作用类似于我们常用 的 Autowired,声明了该注解的属性将会被注入一个路由代理类实例;后者的作用则是 一个配置开关,声明了控制路由的开关属性 架构篇 64  在 RoutingBeanPostProcessor 类中,我们在 postProcessAfterInitialization 方法中通过检 查 bean 中是否存在声明了 RoutingInjected 注解的属性,如果发现存在该注解则给该属 性注入一个动态代理类实例  RoutingBeanProxyFactory 类功能就是生成一个代理类实例,代理类的逻辑也比较简单。 版本路由支持到方法级别,即优先检查方法是否存在路由配置 RoutingSwitch,方法不 存在配置时才默认使用类路由配置 好了,BeanPostProcessor 的介绍就到这里了。不知道看过后大家有没有得到一些启发呢? 欢迎大家留言交流反馈。 架构篇 65 携程在线风控系统架构 [作者简介]蒋一新,携程风险控制部技术专家,负责风控运营平台、数据分发、关系图谱、 名单服务等多个风控子系统;风控引擎性能优化主要实施人员。 为了应对日益严重的支付欺诈,携程在线风控系统 2011 年正式上线。现在,在线风控系统 支撑了携程每日 1 亿+的风险事件实时处理和 100 亿+的准实时数据预处理;系统中运行的 总规则数和总模型数分别达到了 1 万+和 20+;风控的范围从单纯的支付风控扩展到了各种 类型的业务风控(例如:恶意抢占资源、黄牛抢购、商家刷单)。 下图是当前在线风控系统的整体技术架构图: 当前的系统结构是比较主流的风控系统结构,包含了决策引擎、Counter、名单库、用户画 像、离线处理、离线分析和监控各主要模块。携程的在线风控系统发展到这个阶段一共经过 了 3 次重大的改版。 一、最初创立 2011 年上线,使用了.net 开发的服务,数据库使用 SQL Server,简要架构图如下: 架构篇 66 这个时候的风控服务将所有在线决策功能整合在一个系统内实现,包括规则判断、名单库、 流量计算;而这些逻辑都基于数据库实现。 流量计算:通过明细表执行 SQL 得到(例如:SELECT COUNT(DISTINCT orderId) FROMt1 WHERE …[2]) 规则判断:数据库记录大于、小于、等于等判断规则,接收到风险事件后获取流量值和规则 进行比较,得到最终的风险判断。 名单库判断:数据库维护黑白名单信息(属性类型、属性值、判断依据等),程序判断风险 事件中的值是否命中名单。 基于当时携程对风控的需求,系统以满足功能为主。在上线运行一段时间后,随着携程业务 的增长,风控系统的流量不断增加,基于 SQL 的流量统计耗时严重制约了系统的响应时间, 因此有了第一次的性能优化改版。 二、流量查询性能优化改版 由于这个时候的主要性能瓶颈在于数据库实现的流量查询,这次优化主要方向就是优化流量 查询的实现:在原来单个数据库的基础上,采用分库分表的方式均摊压力,以达到更快的响 应时间和更高的吞吐量。架构图如下: 架构篇 67 优化之后,流量库和业务库分离,流量库使用多个数据库实例,使用 hash 的方式拆分流量 明细,统计的时候使用 SQL。由于压力被多个数据库实例分摊,使得系统的流量查询性能得 到了较好的提升。 新版本上线后,携程的业务又对风控系统提出了更多的要求: 1.更方便快捷的接入:除了支付风险,业务的风险也需要风控支持; 2.更多的外部数据接入:用户信息、位置信息、UBT 信息; 3.更丰富的规则逻辑:支持任意变量的规则判断,支持更多的判断逻辑; 4.更高的性能:流量 10x 的增长,响应时间不超过 1 秒; 5.编程语言的更新:携程推动公司内.net 转 java。 然后,就有了奠定当前在线风控系统基础的重大改版 三、风控 3.0(Aegis) 从这个版本开始,风控系统全面转向 Java 开发,同时将核心模块独立成服务,定义了各子 系统的边界以及在整个系统中的定位和作用。相比之前功能性的应用,Aegis 是一个平台化 的风控系统。以下是简化的系统架构图: 架构篇 68 Aegis 开始使用 Drools 脚本用于规则的编写,极大的提高了规则团队对突发事件的响应时 间,紧急规则一般 10 分钟就能上线。 在结构上风控引擎分为同步引擎和异步引擎,同步引擎运行用于实时判断风险结果的规则和 模型;异步引擎则负责验证规则/模型、数据分发、关键数据落地等逻辑。同步/异步引擎设 计成无状态的,方便随时扩容。 Aegis 在流量统计上自研发了 Counter Server,这是一个定制化的类 TSDB 服务,任意精度任 意时间窗口的查询控制在 5ms,同时支持高并发查询,相较于 SQL 的实现提升了上千倍的 性能。支撑了现在风控系统内个服务每日百亿次的查询。下图是其简要的结构说明: 架构篇 69 在数据预处理层面独立出了风险画像和 DataProxy 两个服务。 风险画像服务为引擎提供实时的用户、订单的画像(用户等级、用户行为标签、订单资源标 签等)作为规则和模型的输入变量;其数据来源是实时的引擎数据、准实时的 MQ 数据清洗 服务、离线的数据导入三部分。 DataProxy 服务包装了所有对外的接口和数据库的访问,并针对数据特性的不同配置了不同 的缓存策略,保证 99.9%的请求在 10ms 内获取到所需的数据。 此外还有以下几个主要的服务: 名单库服务,支持多个独立的名单库,优化了名单判断逻辑,使得单次查询(10 个维度)的 响应小于 10ms。 配置服务,集中系统内各个应用需要配置的功能,提供中心化的配置服务让各个应用获取响 应配置。 事件处理平台,用于处理引擎无法判断或需要人工干预的事件。 性能监控服务,监控系统内各服务的健康状态,提供预警和报警功能。 业务监控服务,监控规则模型运行情况、返回的风险结果、事件耗时等业务层面的数据,提 供预警和报警功能。 Aegis 系统上线后,新风控业务接入时间缩短到一周,在 10x 流量增加和执行更多更复杂的 规则的情况下,平均耗时控制在 300+ms,比上一版本提高了 1 倍多。 之后 Aegis 家族又增加了两个重要的子系统:Sessionizer 和 DeviceID。这两个服务属于准实 时处理应用,但是都通过预热的方式为引擎提供了实时数据。 Sessionizer,归约用户的页面访问 session 为反应用户的操作的 RiskSession,流程如下图: 架构篇 70 其数据处理使用了自研的大数据处理系统 Chloro,架构图如下: DeviceID 服务,用于指纹数据采集和指纹识别生成,从而判断设备的唯一性,同样也借助了 Chloro 进行数据处理,系统示意图如下: 架构篇 71 这两个服务为规则和模型以及人工处理提供了非常关键的判断数据,进一步提高了风险判断 的准确性。 只此,Aegis 系统的功能已比较完善,但在 1 年多的运行过程中发现随着流量和规则、模型 量的持续增长,实时风控的响应时间出现缓慢下滑的趋势,超时(1 秒)的量在千分之一上 下,然后就有了之后持续的性能优化。 四、针对实时风控的性能优化 性能优化从 1 年前开始,可以分以下几个主要优化: 1、规则分布式并行执行 架构篇 72 将单个事件需要执行的规则和模型拆分到同一逻辑组内多个服务器执行,最终合并数据。这 个优化将平均耗时降到了 200+ms 2、脚本执行引擎切换 Drools 到 Java 使用模版屏蔽编写规则时 drools 的特殊语法,之后将脚本编译成 Java class 执行。规则的执 行性能提升 1 倍,整个引擎的实时平均耗时降入 100ms。 3、开发 Java 的模型执行引擎 已完成随机森林和逻辑回归算法的 Java 版,相较使用 Python,提高了一个数量级的性能。 完成上述优化后,整个系统在短期内,只需要简单的增加服务器,就可以满足容量扩张。 以上就是 Aegis 系统的架构和演进过程,当然演进的过程还在持续,现阶段的目标是将平台 化的系统继续发展,做到服务产品化。 架构篇 73 分布式架构系统生成全局唯一序列号的一个思路 [作者简介]丁宜人,10 年 java 开发经验。携程技术中心基础业务研发部用户中心资深 java 工程师,负责携程账号的基础服务和相关框架组件研发。之前在惠普公司供职 6 年,负责消 息中间件产品研发。 一、相关背景 分布式架构下,唯一序列号生成是我们在设计一个系统,尤其是数据库使用分库分表的时候 常常会遇见的问题。当分成若干个 sharding 表后,如何能够快速拿到一个唯一序列号,是 经常遇到的问题。 在携程账号数据库迁移 MySql 过程中,我们对用户 ID 的生成方案进行了新的设计,要求能 够支撑携程现有的新用户注册体量。 本文通过携程用户 ID 生成器的实现,希望能够对大家设计分库分表的唯一 id 有一些新的思 路。 二、特性需求 全局唯一 支持高并发 能够体现一定属性 高可靠,容错单点故障 高性能 三、业内方案 生成 ID 的方法有很多,来适应不同的场景、需求以及性能要求。 常见方式有: 1、利用数据库递增,全数据库唯一。 优点:明显,可控。 缺点:单库单表,数据库压力大。 2、UUID, 生成的是 length=32 的 16 进制格式的字符串,如果回退为 byte 数组共 16 个 byte 元素,即 UUID 是一个 128bit 长的数字,一般用 16 进制表示。 优点:对数据库压力减轻了。 缺点:但是排序怎么办? 架构篇 74 此外还有 UUID 的变种,增加一个时间拼接,但是会造成 id 非常长。 3、twitter 在把存储系统从 MySQL 迁移到 Cassandra 的过程中由于 Cassandra 没有顺序 ID 生成机制,于是自己开发了一套全局唯一 ID 生成服务:Snowflake。 1 41 位的时间序列(精确到毫秒,41 位的长度可以使用 69 年) 2 10 位的机器标识(10 位的长度最多支持部署 1024 个节点) 3 12 位的计数顺序号(12 位的计数顺序号支持每个节点每毫秒产生 4096 个 ID 序号) 最 高位是符号位,始终为 0。 优点:高性能,低延迟;独立的应用;按时间有序。 缺点:需要独立的开发和部署。 4、Redis 生成 ID 当使用数据库来生成 ID 性能不够要求的时候,我们可以尝试使用 Redis 来生成 ID。这主要 依赖于 Redis 是单线程的,所以也可以用生成全局唯一的 ID。可以用 Redis 的原子操作 INCR 和 INCRBY 来实现。 可以使用 Redis 集群来获取更高的吞吐量。假如一个集群中有 5 台 Redis。可以初始化每台 Redis 的值分别是 1,2,3,4,5,然后步长都是 5。各个 Redis 生成的 ID 为: A:1,6,11,16,21 B:2,7,12,17,22 C:3,8,13,18,23 D:4,9,14,19,24 E:5,10,15,20,25 比较适合使用 Redis 来生成每天从 0 开始的流水号。比如订单号=日期+当日自增长号。可 以每天在 Redis 中生成一个 Key,使用 INCR 进行累加。 优点: 不依赖于数据库,灵活方便,且性能优于数据库。 数字 ID 天然排序,对分页或者需要排序的结果很有帮助。 使用 Redis 集群也可以防止单点故障的问题。 缺点: 如果系统中没有 Redis,还需要引入新的组件,增加系统复杂度。 需要编码和配置的工作量比较大,多环境运维很麻烦, 在开始时,程序实例负载到哪个 redis 实例一旦确定好,未来很难做修改。 5、Flicker 的解决方案 架构篇 75 因为 MySQL 本身支持 auto_increment 操作,很自然地,我们会想到借助这个特性来实现这 个功能。 Flicker 在解决全局 ID 生成方案里就采用了 MySQL 自增长 ID 的机制(auto_increment + replace into + MyISAM)。 6.还有其他一些方案,比如京东淘宝等电商的订单号生成。因为订单号和用户 id 在业务上 的区别,订单号尽可能要多些冗余的业务信息,比如: 滴滴:时间+起点编号+车牌号 淘宝订单:时间戳+用户 ID 其他电商:时间戳+下单渠道+用户 ID,有的会加上订单第一个商品的 ID。 而用户 ID,则要求含义简单明了,包含注册渠道即可,尽量短。 四、最终方案 最终我们选择了以 flicker 方案为基础进行优化改进。具体实现是,单表递增,内存缓存号段 的方式。 首先建立一张表,像这样: SEQUENCE_GENERATOR_TABLE id stub 1 192.168.1.1 其中 id 是自增的,stub 是服务器 ip 因为新数据库采用 mysql,所以使用 mysql 的独有语法 replace to 来更新记录来获得唯一 id,例如这样: REPLACE INTO uidgenerator (stub) VALUES ("10.32.25.8"); 该语句的含义,是按主键,存在就更新这条记录,不存在就 insert 这条记录。再用 SELECT id FROM uidgenerator WHERE stub = "10.32.25.8";把它拿回来 到上面为止,我们只是在单台数据库上生成 ID,从高可用角度考虑,接下来就要解决单点故 障问题 这也就是为什么要有这个机器 ip 字段呢?就是为了防止多服务器同时更新数据,取回的 id 混淆的问题。 所以,当多个服务器的时候,这个表是这样的: 架构篇 76 id stub 5 10.32.25.8 2 10.3.2.132 3 10.32.25.7 4 10.2.82.65 每台服务器只更新自己的那条记录,保证了单线程操作单行记录。 这时候每个机器拿到的分别是 5,2,3,4 这 4 个 id。 至此,我们似乎解决这个服务器隔离,原子性获得 id 的问题,也和 flicker 方案基本一致。 但是原理上,我们还是依靠数据库的特性,每次生成 id 都要请求 db,开销很大。我们对此 又进行优化: 把取回 id 转换为一个号段,进行内存缓存,并且这个号段是可以配置长度的,可以 1000 也 可以 10000,也就是对拿回来的这个 id 放大多少倍的问题。 OK,我们从 DB 一次查询操作,拿回来了 1000 个 id 到内存中了,酷! 有同学会问,进行缓存的话,多台服务器时候,会不会拿到重复的 id? 这时候我们再来看一下数据库的那张表的数据。 id stub 5 10.32.25.8 2 10.3.2.132 3 10.32.25.7 4 10.2.82.65 4 条数据对应 4 台服务器,每条数据的主键 id 是不同的,那么对应的号段也就是永远没有 交集的了。 在解决了多服务器隔离的问题后,现在的问题就是要解决同一台服务器在高并发场景,让大 家顺序拿号,别拿重复,也别拿丢了。 这个问题简单来说,就是个保持这个号段对象隔离性的问题咯? AtomicLong 是个靠谱的办法。 当第一次拿回号段 id 后,扩大 1000 倍,然后赋值给这个变量 atomic,这就是这个号段的第 一个号码。 atomic.set(n * 1000); 架构篇 77 并且内存里保存一下最大 id,也就是这个号段的最后一个号码 currentMaxId = (n + 1) * 1000; 一个号段就形成了。 此时每次有请求来取号时候,判断一下有没有到最后一个号码,没有到,就拿个号,走人。 Long uid = atomic.incrementAndGet(); 如果到达了最后一个号码,那么阻塞住其他请求线程,最早的那个线程去 db 取个号段,再 更新一下号段的两个值,就可以了。 这个方案,核心代码逻辑不到 20 行,解决了分布式系统序列号生成的问题。 这里有个小问题,就是在服务器重启后,因为号码缓存在内存,会浪费掉一部分用户 ID 没 有发出去,所以在可能频繁发布的应用中,尽量减小号段放大的步长 n,能够减少浪费。 经过实践,性能的提升远远重要于浪费一部分 id。 如果再追求极致,可以监听 spring 或者 servlet 上下文的销毁事件,把当前即将发出去的用 户 ID 保存起来,下次启动时候再捞回内存即可。 五、上线效果 运行 5 个多月,十分稳定。 SOA 服务平均响应时间 0.59 毫秒; 客户端调用平均响应时间 2.52 毫秒; 附流程图: 架构篇 78 架构篇 79 如何实现金服业务流程动态化 [作者简介]赫杰辉,开源框架 x-series 作者,携程 dal 框架贡献者,认为传统开发模式已经 无法适应新的时代,相信开发工具是提高效率和质量的关键因素。业余时间喜欢玩躺车,单 鳍脚蹼等古怪玩意。 怀立德,从事金融行业技术 10 年,对能让设计、开发工作变得更轻松的技术都有兴趣。毕 生最大的目标就是如何让程序猿变得更懒,如今随着体重的逐渐上升,已略有小成。 本文通过介绍携程在互联网金融服务领域如何解决业务流程合规、快速和高效落地的问题, 提出了一种基于开源框架 xstate 的工作流实施和动态调整方案。这种方案适合需要在线流 程升级的应用,包括金融、证券、电商、政务和物流等行业领域。 背景 用户资格在线审核是个复杂的流程,包含多个步骤与判断。这些流程,步骤和判断都必须符 合一定国家的法律、规则和准则,简称合规。之前的做法是通过代码来硬编码审核流程的逻 辑。当政策发生变化时,为了继续保持合规必须修改代码,测试和重新发布。这种做法的困 难之处在于:  开发与维护难度大。对复杂逻辑做硬编码会导致对应的代码也非常复杂。通过读代码的 方式来理解系统很困难,而修改老系统非常容易破坏正确逻辑或者引进新的 bug,这种 系统一般没人愿意接手。  上线时间难以保证。新合规何时出现是无法预测的,而出现时却往往会规定较紧迫的上 线时间。在开发任务非常饱满,开发进度非常紧张的时候,很难为新合规上线调整计划。 据说曾经出现过新合规虽然发布并规定了上线时间,但由于各个公司实在无法按时完成 开发,最终不得不推迟上线的情况。  无法保证系统质量。现代互联网公司往往是大规模并行开发,有固定的发布节奏,并且 一次多个功能同时发布上线是常态。这种情况下,新合规功能可能会与有缺陷的功能一 同上线,当缺陷版本回退的时候,会导致合规功能一起回退。这种功能间的耦合必然会 影响上线效果。 由于老办法有上述这些问题,携程金服团队准备对目前的业务进行重构,考虑引入类工作流 来完成业务流程。 解决方案 在金融团队评估各种方案时,我向他们推荐了 xstate,一个轻量级状态机编辑和运行框架。 它是 x-series 快速开发框架中的一个组件。 xstate 包括一个基于 Eclipse 的可视化模型编辑器和一个独立的引擎。用户在 Eclipse 里用 架构篇 80 xstate 定制的状态图编辑器构建模型文件;应用程序调用 xstate 引擎读取模型文件并在内 存中创建状态机实例;运行时通过生成事件来触发实例的状态变迁,从而实现流程的推进并 触发相应逻辑。 xstate 仅包含状态、变迁、事件和触发器等和状态机直接相关的最小核心概念。即可以不写 一行代码实现一个可以运行的状态机,又可以用自定义的触发器灵活扩展和组织复杂逻辑。 对比其它工作流引擎,如 activiti,JBPM,虽然这几款开源软件都能完成业务需求,但 xstate 有以下几个特点:  无环境依赖。对数据库和环境都无特殊要求。相比其它引擎需要一系列的搭建工作,如 建表、编写配置文件等,xstate 只需引入 jar 包就可直接使用。  快速上手。一款框架产品可以快速上手是非常重要的。xstate 是一款非常轻量级的基于 状态机的框架,通过阅读文档,运行 sample 即可快速了解整个框架的运作机制,从而 进行自己的开发工作,十分符合目前互联网环境快速迭代的开发节奏。  快速融入现有系统。xstate 提供了状态变迁所需要的所有基本步骤,使用配套的可视化 工具可以快速搭建一套包含各个业务节点的工作流。通过简单配置可以迅速绑定节点和 指定业务代码的关系,无需对已有的业务代码进行重构。这大大降低了二次开发的成本。  可视化。xstate 配套的工具包括可视化界面,能快速完成功能设计,同时如上述第三点, 通过配置节点和业务代码的关系,简单的操作就串起整个业务流程。 高度集成、可视化的开发环境 架构篇 81 xstate 的开发宗旨是要打造一个高度集成、可视化的开发环境,让用户在开发过程中无需在 不同的环境中来回切换,减少工作中的停顿,从而高效工作。因此 xstate 被开发为基于 Eclipse GEF 技术的插件。Eclipse 对插件有一套完整的框架进行管理,用户只需按照标准安 装步骤简单地将插件安装到 Eclipse 中,就会拥有一个与 IDE 紧密结合的工作环境。 具体安装步骤可以参考安装指南: https://github.com/hejiehui/xross-tools- installer#%E5%AE%89%E8%A3%85%E6%AD%A5%E9%AA%A4 由于 xstate 的开发主要用到 Eclipse 的 GEF 技术。为了方便大家理解,这里简要的介绍一 下 GEF。GEF(Graphical Editing Framework)是一个图形化编辑框架,它允许开发人员以图 形化的方式展示和编辑模型,从而提升用户体验。 GEF 最早是 Eclipse 的一个内部项目,后来逐渐转变为 Eclipse 的一个开源工具项目, Eclipse 的不少其它子项目都需要它的支持。有很多应用天然适合图形化展示,包括 UML 类 图编辑器、图形化 XML 编辑器、界面设计工具以及图形化数据库结构设计工具等等。 GEF 的优势是提供了标准的 MVC(Model-View-Control)结构,开发人员可以利用 GEF 来 完成以上这些功能,而不需要自己重新设计。与其他一些 MVC 编辑框架相比,GEF 的一个 主要设计目标是尽量减少模型和视图之间的依赖,好处是可以根据需要选择任意模型和视图 的组合,而不必受开发框架的局限。 架构篇 82 GEF 模型只与控制器打交道,而不知道任何与视图有关的东西。为了能让控制器知道模型的 变化,应该把控制器作为事件监听者注册在模型中,当模型发生变化时,就触发相应的事件 给控制器,后者负责通知各个视图进行更新。 GEF 控制器是模型与视图之间的桥梁,也是整个 GEF 的核心。它不仅要监听模型的变化, 当用户编辑视图时,还要把编辑结果反映到模型上。举个例子来说,用户在数据库结构图上 删除一个表时,控制器应该从模型中删除这个表对象、表中的字段对象、以及与这些对象有 关的所有连接。 在这个案例中,用户可以利用 xstate 对审核流程建模,并用 xstate 在线上实际运行流程处 理和逻辑判断。在进一步沟通了解之后,携程金融团队认为该框架符合他们的各项要求,并 在现有系统改造和新合规发布中加以采用。 效果 采用 xstate 后,当新合规下发时,大部分情况下,开发人员只需在 IDE 里面打开已有 xstate 模型文件,在交互式的图形编辑器里面以所见即所得的方式修改状态机即可完成开发。测试 人员可以通过状态机的可视化模型进行直观的理解和验证测试。 测试通过后,模型可以单独发布到携程的配置中心,无需应用重新发布或重启。应用在监听 到变更请求时,通知 xstate 引擎读取新的模型数据并创建模型,即可实现合规的在线发布 或升级。 使用 xstate 后,合规的开发与维护做到了简单高效,发布做到了灵活即时,最终的实际效 果让人满意。 下图是一个业务功能重构中,流程图在 xstate 中的显示效果: 同时 xstate 还吸收了用户在使用过程中提出的改进意见,促进了框架的进一步完善,取得 了双赢的效果。 参考资料: X-Series 工具集:https://github.com/hejiehui/xross-tools-installer 技术支持群: 架构篇 83 架构篇 84 深入理解 Python 装饰器 [作者简介]曾凡伟,携程信息安全部高级安全工程师,2015 年加入携程,主要负责安全自动 化产品的设计和研发,包括各类扫描器、漏洞管理平台、安全 SaaS 平台等。 Python 是一门追求优雅编程的语言,它很容易上手,也很容易写出意大利式的代码。本文 将介绍如何使用 Python 进阶编程之装饰器,来帮助您写出更加精炼可读的代码。 全文主要分为四个部分: 第一部分:尝鲜,通过讲解一个简单的装饰器例子,让您对装饰器的用法和作用有一个初步 的感性认识; 第二部分:揭开面纱,将介绍装饰器抛开语法糖的使用方法,帮助您理解装饰器的本质原理; 第三部分:趁热打铁,将介绍装饰器在工作当中的实践用法,对应介绍的 retry 装饰器您可 直接应用到项目代码中; 第四部分:更进一步,将介绍装饰器更多的高级用法,帮助您全方位掌握装饰器。 尝鲜 我们先来看一个简单的装饰器例子。首先定义一个装饰器 log: def log(f): def wrapper(): print "before" f() print "after" return wrapper 使用装饰器 log 来装饰 greeting 函数,并调用之: @log def greeting(): print "Hello, World!" greeting() 输出结果: 架构篇 85 before Hello, World! after 可以看到,使用装饰器我们实现了在函数 greeting 前后打印调试日志。 揭开面纱 装饰器是什么?从字面意思我们大致可以推测出来,它的作用是用来装饰的。日常生活中, 大家都见过很多装饰器,比如装饰在圣诞树上的彩纸,或者套在 iPhone 外面的保护壳。保 护壳的存在,并不会改变 iPhone 内部的功能,它存在的意义,在于增强了 iPhone 的抗摔性 能。Python 中的装饰器也是一样的道理,它并不会改变被装饰对象的内部逻辑,而是通过 一种无侵入的方式,让它获得一些额外的能力,比如日志记录、权限认证、失败重试等等。 Python 装饰器看起来高深莫测,实际上它的实现原理非常简单。我们知道,在 Python 中一 切皆对象,函数作为一个特殊的对象,可以作为参数传递给另外一个函数,装饰器的工作原 理就是基于这一特性。装饰器的默认语法是使用@来调用,这实际上仅仅是一种语法糖。下 面我们看看,不利用语法糖来怎么调用装饰器: def greeting(): print "Hello, World!" greeting = log(greeting) 把函数 greeting 作为参数传递给装饰器函数 log 就行了!对装饰器 log 来说,它接收一个函 数作为入参,然后返回一个新的函数,最后再赋值给 greeting 标识符。这样便得到了一个增 强功能的函数,而它的名字又和之前的保持一样。 趁热打铁 装饰器是一个编程利器,只需一处修改,任何被装饰的对象就可以获得额外的功能。撸起袖 子,让我们来看看装饰器在编程实践中的具体应用。 我们知道,程序跑起来后,有一些因素往往是不可控的,比如网络的连通性。为了容错,我 们可能会加入 try-except 语句来捕获异常;考虑到请求失败是有一定概率的,我们或许可以 通过多次重试的策略,以达到提高成功率的目的。我们先来模拟一个 non_steady 函数: import random def non_steady(): if random.random() <= 0.5: # 失败的概率是 0.5 raise Exception("died") else: 架构篇 86 # 成功的概率是 0.5 return "survived" 这个函数成功返回的概率是 0.5。显然,单次调用的成功率太低,如果重试 10 次呢?计算一 下:1 - (0.5) ^ 10,即成功的概率将提升到 0.9990,相比单次调用的 0.5,重试的成功率大 大地提升了。 按照上面的描述,我们先通过 for 循环来提升调用 non_steady 的成功率: def non_steady_with_retry(times=10): for i in xrange(times): try: return non_steady() except Exception as e: if (i + 1) < times: # 尚未达到最大重试次数,默默吞掉异常 pass else: # 连续重试,达到最大次数时还是发生异常,则抛出异常 raise e 提升成功率的效果达到了,但是这种实现存在几个问题: 1、不支持无缝升级。假如函数 non_steady 在代码中被调用了 n 次,那么这意味着你需要同 时修改 n 个地方(将调用 non_steady 修改为调用 non_steady_with_retry); 2、不支持代码复用。如果你有第二个函数 non_steady1,也需要升级一下重试机制,那么这 意味着同样的重试代码,你需要再重写一遍。 再试试用装饰器来提升调用 non_steady 的成功率。定义一个 retry 装饰器: def retry(times=10): def outer(f): def inner(*args, **kwargs): for i in xrange(times): try: return f(*args, **kwargs) except Exception as e: if (i + 1) < times: pass else: raise e return inner 架构篇 87 return outer 试用一下: import random @retry(10) def non_steady(): if random.random() <= 0.5: # 失败的概率是 0.5 raise Exception("died") else: # 成功的概率是 0.5 return "survived" 可以看到,只要函数前面加一行代码@retry(10),即可为其升级重试机制。一处更改即可, 无需处处担忧。同时,对于其他想要升级的函数,也只需要更改一个地方,同样的代码就无 需重写多遍了。 更进一步 一个函数可以同时应用多个装饰器,比如下面使用两个装饰器来装饰 greeting 函数: @log @retry(10) def greeting(): print "Hello, World!" 这段代码等价于: def greeting(): print "Hello, World!" temp = retry(10)(greeting) greeting = log(temp) 可以看到,叠加的装饰器生效的顺序是从内往外的。这一点在使用的时候需要特别注意。 Java 中的注解,语法和 Python 中的装饰器很相似,它注解的顺序,没有 Python 中装饰器这 么严格。使用时注意区分下。 除了函数,也可以用类来定义一个装饰器: 架构篇 88 class Log(object): def __init__(self, f): self.f = f def __call__(self, *args, **kwargs): print "before" self.f() print "after" 类装饰器主要是通过它的__call__方法来实现的。相比函数装饰器,类装饰器具有面向对象编 程所支援的一系列特点,比如高内聚、封装性和灵活度大等优点。使用类装饰器来装饰函数: @Log def greeting(): print "Hello, World!" greeting() 输出结果和使用函数装饰器一样: before Hello, World! after 实际上,Python 中任何 callable 的对象都可以用来定义装饰器。 结语 使用 Python 装饰器,可以让你的代码更易维护,可读性也有一定提升。相信大家在日常工 作中也有碰到过很多使用装饰器的场景,欢迎留言分享!人生苦短,我用 Python。 架构篇 89 携程实时用户行为系统实践 [作者简介]陈清渠,毕业于武汉大学,多年软件及互联网行业开发经验。14 年加入携程,先 后负责了订单查询服务重构,实时用户行为服务搭建等项目的架构和研发工作,目前负责携 程技术中心基础业务研发部订单中心团队。 携程实时用户行为服务作为基础服务,目前普遍应用在多个场景中,比如猜你喜欢(携程的 推荐系统),动态广告,用户画像,浏览历史等等。 以猜你喜欢为例,猜你喜欢为应用内用户提供潜在选项,提高成交效率。旅行是一项综合性 的需求,用户往往需要不止一个产品。作为一站式的旅游服务平台,跨业务线的推荐,特别 是实时推荐,能实际满足用户的需求,因此在上游提供打通各业务线之间的用户行为数据有 很大的必要性。 携程原有的实时用户行为系统存在一些问题,包括:1)数据覆盖不全;2)数据输出没有统 一格式,对众多使用方提高了接入成本;3)日志处理模块是 web service,比较难支持多种 数据处理策略和实现方便扩容应对流量洪峰的需求等。 而近几年旅游市场高速增长,数据量越来越大,并且会持续快速增长。有越来越多的使用需 求,对系统的实时性,稳定性也提出了更高的要求。总的来说,当前需求对系统的实时性/ 可用性/性能/扩展性方面都有很高的要求。 一、架构 这样的背景下,我们按照如下结构重新设计了系统: 架构篇 90 图 1 实时用户行为系统逻辑视图 新的架构下,数据有两种流向,分别是处理流和输出流。 在处理流,行为日志会从客户端(App/Online/H5)上传到服务端的 Collector Service。 Collector Service 将消息发送到分布式队列。数据处理模块由流计算框架完成,从分布式队 列读出数据,处理之后把数据写入数据层,由分布式缓存和数据库集群组成。 输出流相对简单,web service 的后台会从数据层拉取数据,并输出给调用方,有的是内部 服务调用,比如推荐系统,也有的是输出到前台,比如浏览历史。系统实现采用的是 Java+Kafka+Storm+Redis+Mysql+Tomcat+Spring 的技术栈。  Java:目前公司内部 Java 化的氛围比较浓厚,并且 Java 有比较成熟的大数据组件  Kafka/Storm:Kafka 作为分布式消息队列已经在公司有比较成熟的应用,流计算框架 Storm 也已经落地,并且有比较好的运维支持环境。  Redis: Redis 的 HA,SortedSet 和过期等特性比较好地满足了系统的需求。  MySQL: 作为基础系统,稳定性和性能也是系统的两大指标,对比 nosql 的主要选项, 比如 hbase 和 elasticsearch,十亿数据级别上 mysql 在这两方面有更好的表现,并且经 过设计能够有不错的水平扩展能力。 目前系统每天处理 20 亿左右的数据量,数据从上线到可用的时间在 300 毫秒左右。查询服 务每天服务 8000 万左右的请求,平均延迟在 6 毫秒左右。下面从实时性/可用性/性能/部署 几个维度来说明系统的设计。 二、实时性 架构篇 91 作为一个实时系统,实时性是首要指标。线上系统面对着各种异常情况。例如如下几种情况:  突发流量洪峰,怎么应对;  出现失败数据或故障模块,如何保证失败数据重试并同时保证新数据的处理;  环境问题或 bug 导致数据积压,如何快速消解;  程序 bug,旧数据需要重新处理,如何快速处理同时保证新数据; 系统从设计之初就考虑了上述情况。 首先是用 storm 解决了突发流量洪峰的问题。storm 具有如下特性: 图 2:Storm 特性 作为一个流计算框架,和早期大数据处理的批处理框架有明显区别。批处理框架是执行完一 次任务就结束运行,而流处理框架则持续运行,理论上永不停止,并且处理粒度是消息级别, 因此只要系统的计算能力足够,就能保证每条消息都能第一时间被发现并处理。 对当前系统来说,通过 storm 处理框架,消息能在进入 kafka 之后毫秒级别被处理。此外, storm 具有强大的 scale out 能力。只要通过后台修改 worker 数量参数,并重启 topology (storm 的任务名称),可以马上扩展计算能力,方便应对突发的流量洪峰。 对消息的处理 storm 支持多种数据保证策略,at least once,at most once,exactly once。 对实时用户行为来说,首先是保证数据尽可能少丢失,另外要支持包括重试和降级的多种数 据处理策略,并不能发挥 exactly once 的优势,反而会因为事务支持降低性能,所以实时用 户行为系统采用的 atleast once 的策略。这种策略下消息可能会重发,所以程序处理实现了 幂等支持。 架构篇 92 storm 的发布比较简单,上传更新程序 jar 包并重启任务即可完成一次发布,遗憾的是没有 多版本灰度发布的支持。 图 3:Storm 架构 在部分情况下数据处理需要重试,比如数据库连接超时,或者无法连接。连接超时可能马上 重试就能恢复,但是无法连接一般需要更长时间等待网络或数据库的恢复,这种情况下处理 程序不能一直等待,否则会造成数据延迟。实时用户行为系统采用了双队列的设计来解决这 个问题。 架构篇 93 图 4:双队列设计 生产者将行为纪录写入 Queue1(主要保持数据新鲜),Worker 从 Queue1 消费新鲜数据。 如果发生上述异常数据,则 Worker 将异常数据写入 Queue2(主要保持异常数据)。 这样 Worker 对 Queue1 的消费进度不会被异常数据影响,可以保持消费新鲜数据。 RetryWorker 会监听 Queue2,消费异常数据,如果处理还没有成功,则按照一定的策略(如 下图)等待或者重新将异常数据写入 Queue2。 架构篇 94 图 5:补偿重试策略 另外,数据发生积压的情况下,可以调整 Worker 的消费游标,从最新的数据重新开始消费, 保证最新数据得到处理。中间未经处理的一段数据则启动 backupWorker,指定起止游标, 在消费完指定区间的数据之后,backupWorker 会自动停止。(如下图) 图 6:积压数据消解 架构篇 95 三、可用性 作为基础服务,对可用性的要求比一般的服务要高得多,因为下游依赖的服务多,一旦出现 故障,有可能会引起级联反应影响大量业务。项目从设计上对以下问题做了处理,保障系统 的可用性: 系统是否有单点? DB 扩容/维护/故障怎么办? Redis 维护/升级补丁怎么办? 服务万一挂了如何快速恢复?如何尽量不影响下游应用? 首先是系统层面上做了全栈集群化。kafka 和 storm 本身比较成熟地支持集群化运维;web 服务支持了无状态处理并且通过负载均衡实现集群化;Redis 和 DB 方面携程已经支持主备 部署,使用过程中如果主机发生故障,备机会自动接管服务;通过全栈集群化保障系统没有 单点。 另外系统在部分模块不可用时通过降级处理保障整个系统的可用性。先看看正常数据处理流 程:(如下图) 图 7:正常数据流程 在系统正常状态下,storm 会从 kafka 中读取数据,分别写入到 redis 和 mysql 中。服务从 redis 拉取(取不到时从 db 补偿),输出给客户端。DB 降级的情况下,数据流程也随之改变 (如下图) 架构篇 96 图 8:系统降级-DB 当 mysql 不可用时,通过打开 db 降级开关,storm 会正常写入 redis,但不再往 mysql 写入 数据。数据进入 reids 就可以被查询服务使用,提供给客户端。另外 storm 会把数据写入一 份到 kafka 的 retry 队列,在 mysql 正常服务之后,通过关闭 db 降级开关,storm 会消费 retry 队列中的数据,从而把数据写入到 mysql 中。redis 和 mysql 的数据在降级期间会有不 一致,但系统恢复正常之后会通过 retry 保证数据最终的一致性。redis 的降级处理也类似 (如下图) 架构篇 97 图 9:系统降级-Redis 唯一有点不同的是 redis 的服务能力要远超过 mysql。所以在 redis 降级时系统的吞吐能力是 下降的。这时我们会监控 db 压力,如果发现 mysql 压力较大,会暂时停止数据的写入,降 低 mysql 的压力,从而保证查询服务的稳定。 为了降低故障情况下对下游的影响,查询服务通过 Netflix 的 Hystrix 组件支持了熔断模式 (如下图)。 架构篇 98 图 10:Circuit Breaker Pattern 在该模式下,一旦服务失败请求在给定时间内超过一个阈值,就会打开熔断开关。在开关开 启情况下,服务对后续请求直接返回失败响应,不会再让请求经过业务模块处理,从而避免 服务器进一步增加压力引起雪崩,也不会因为响应时间延长拖累调用方。 开关打开之后会开始计时,timeout 后会进入 Half Open 的状态,在该状态下会允许一个请 求通过,进入业务处理模块,如果能正常返回则关闭开关,否则继续保持开关打开直到下次 timeout。这样业务恢复之后就能正常服务请求。 另外,为了防止单个调用方的非法调用对服务的影响,服务也支持了多个维度限流,包括调 用方 AppId/ip 限流和服务限流,接口限流等。 四、性能&扩展 由于在线旅游行业近几年的高速增长,携程作为行业领头羊也蓬勃发展,因此访问量和数据 量也大幅提升。公司对业务的要求是可以支撑 10 倍容量扩展,扩展最难的部分在数据层, 因为涉及到存量数据的迁移。 实时用户行为系统的数据层包括 Redis 和 Mysql,Redis 因为实现了一致性哈希,扩容时只 要加机器,并对分配到新分区的数据作读补偿就可以。 Mysql 方面,我们也做了水平切分作为扩展的准备,分片数量的选择考虑为 2 的 n 次方,这 样做在扩容时有明显的好处。因为携程的 mysql 数据库现在普遍采用的是一主一备的方式, 在扩容时可以直接把备机拉平成第二台(组)主机。假设原来分了 2 个库,d0 和 d1,都放 在服务器 s0 上,s0 同时有备机 s1。扩容只需要如下几步: 架构篇 99 确保 s0 -> s1 同步顺利,没有明显延迟 s0 暂时关闭读写权限 确认 s1 已经完全同步 s0 更新 s1 开放读写权限 d1 的 dns 由 s0 切换到 s1 s0 开放读写权限 迁移过程利用 mysql 的复制分发特性,避免了繁琐易错的人工同步过程,大大降低了迁移成 本和时间。整个操作过程可以在几分钟完成,结合 DB 降级的功能,只有在 dns 切换的几秒 钟时间会产生异常。 整个过程比较简单方便,降低了运维负担,一定程度也能降低过多操作造成类似 gitlab 式悲 剧的可能性。 五、部署 前文提到 storm 部署是比较方便的,只要上传重启就可以完成部署。部署之后由于程序重新 启动上下文丢失,可以通过 Kafka 记录的游标找到之前处理位置,恢复处理。 另外有部分情况下程序可能需要多版本运行,比如行为纪录暂时有多个版本,这种情况下我 们会新增一个 backupJob,在 backupJob 中运行历史版本。 大数据篇 100 大数据篇 大数据篇 101 机器学习算法线上部署方法 [作者简介]潘鹏举,携程酒店研发 BI 经理,负责酒店服务相关的业务建模工作,主要研究 方向是用机器学习实现业务流程自动化、系统智能化、效率最优化,专注于算法实践和应用。 我们经常会碰到一个问题:用了复杂的 GBDT 或者 xgboost 大大提升了模型效果,可是在上 线的时候又犯难了,工程师说这个模型太复杂了,我没法上线,满足不了工程的要求,你帮 我转换成 LR 吧,直接套用一个公式就好了,速度飞速,肯定满足工程要求。这个时候你又 屁颠屁颠用回了 LR,重新训练了一下模型,心里默骂千百遍:工程能力真弱。 这些疑问,我们以前碰到过,通过不断的摸索,试验出了不同的复杂机器学习的上线方法, 来满足不同场景的需求。在这里把实践经验整理分享,希望对大家有所帮助。(我们的实践 经验更多是倾向于业务模型的上线流程,广告和推荐级别的部署请自行绕道)。 首先在训练模型的工具上,一般三个模型训练工具,Spark、R、Python。这三种工具各有千 秋,以后有时间,我写一下三种工具的使用心得。针对不同的模型使用场景,为了满足不同 的线上应用的要求,会用不同的上线方法: 一、总结来说,大体分这三种场景,请大家对号入座,酌情使用。 1、如果是实时的、小数据量的预测应用,则采用的 SOA 调用 Rserve 或者 python-httpserve 来进行应用;这种应用方式有个缺点是需要启用服务来进行预测,也就是需要跨环境,从 Java 跨到 R 或者 Python 环境。对于性能,基本上我们用 Rserver 方式,针对一次 1000 条或者更 少请求的预测,可以控制 95%的结果在 100ms 内返回结果,100ms 可以满足工程上的实践 要求。更大的数据量,比如 10000/次,100000/次的预测,我们目前评估下来满足不了 100ms 的要求,建议分批进行调用或者采用多线程请求的方式来实现。 2、如果是实时、大数据量的预测应用,则会采用 SOA,训练好的模型转换成 PMML(关于 如何转换,我在下面会详细描述),然后把模型封装成一个类,用 Java 调用这个类来预测。 用这种方式的好处是 SOA 不依赖于任何环境,任何计算和开销都是在 Java 内部里面消耗掉 了,所以这种工程级别应用速度很快、很稳定。用此种方法也是要提供两个东西,模型文件 和预测主类; 3、如果是 Offline(离线)预测的,D+1 天的预测,则可以不用考虑第 1、2 中方式,可以 简单的使用 Rscript x.R 或者 python x.py 的方式来进行预测。使用这种方式需要一个调度 工具,如果公司没有统一的调度工具,你用 shell 的 crontab 做定时调用就可以了。 以上三种做法,都会用 SOA 里面进行数据处理和变换,只有部分变换会在提供的 Function 或者类进行处理,一般性都建议在 SOA 里面处理好,否则性能会变慢。 大概场景罗列完毕,简要介绍一下各不同工具的线上应用的实现方式; 大数据篇 102 二、如何转换 PMML,并封装 PMML 大部分模型都可以用 PMML 的方式实现,PMML 的使用方法调用范例见: jpmml 的说明文档:GitHub-jpmml/jpmml-evaluator: Java Evaluator API for PMML Java 调用 PMML 的范例,此案例是我们的工程师写的范例,大家可以根据此案例进行修改 即可; Jpmml 支持的转换语言,主流的机器学习语言都支持了,深度学习类除外; 从下图可以看到,它支持 R、python 和 spark、xgboost 等模型的转换,用起来非常方便; 三、各个算法工具的工程实践 python 模型上线:目前使用了模型转换成 PMML 上线方法; python-sklearn里面的模型都支持,也支持xgboost,并且PCA,归一化可以封装成preprocess 转换成 PMML,所以调用起来很方便。 特别需要注意的是:缺失值的处理会影响到预测结果,大家可以可以看一下 用 PMML 方式预测,模型预测一条记录速度是 1ms,可以用这个预测来预估一下根据你的 数据量,整体的速度有多少; R 模型上线-这块我们用的多,可以用 R model 转换 PMML 的方式来实现。 大数据篇 103 这里我介绍另一种的上线方式:Rserve。 具体实现方式是:用 SOA 调用 Rserve 的方式去实现,我们会在服务器上部署好 R 环境和安 装好 Rserve,然后用 JAVA 写好 SOA 接口,调用 Rserve 来进行预测; java 调用 Rserve 方式见网页链接:Rserve - Binary R server centos 的 Rserve 搭建方法见:centos -Rserve 的搭建,这里详细描述了 Rserve 的搭建方式; Rserve 方式可以批量预测,跟 PMML 的单个预测方式相比,在少数据量的时候,PMML 速 度更快,但是如果是 1000 一次一批的效率上看,Rserve 的方式会更快; 用 Rserve 上线的文件只需要提供两个: - 模型结果文件(XX.Rdata) - 预测函数(Pred.R); Rserve_1 启动把模型结果(XX.Rdata)常驻内存。预测需要的输入 Feature 都在 Java 里定义好 不同的变量,然后你用 Java 访问 Rserve_1,调用 Pred.R 进行预测,获取返回的 List 应用在 线上。最后把相关的输入输出存成 log 进行数据核对。 Pred.R <- function(x1,x2,x3){ data <- cbind(x1,x2,x3) # feature engineering score <- predict(modelname, data, type = 'prob') return(list(score)) } Spark 模型上线-好处是脱离了环境,速度快; Spark 模型的上线就相对简单一些,我们用 scala 训练好模型(一般性都用 xgboost 训练模 型)然后写一个 Java Class,直接在 JAVA 中先获取数据,数据处理,把处理好的数据存成 一个数组,然后调用模型 Class 进行预测。模型文件也会提前 load 在内存里面,存在一个进 程里面,然后我们去调用这个进程来进行预测。所以速度蛮快的。 Spark 模型上线,放在 spark 集群,不脱离 spark 环境,方便,需要自己打 jar 包; 我们这里目前还没有尝试过,有一篇博客写到了如果把 spark 模型导出 PMML,然后提交到 spark 集群上来调用,大家可以参考一下:Spark 加载 PMML 进行预测 四、只用 Linux 的 Shell 来调度模型的实现方法-简单粗暴; 因为有些算法工程师想快速迭代,把模型模拟线上线看一下效果,所以针对离线预测的模型 形式,还有一种最简单粗暴的方法,这种方法开发快速方便,具体做法如下: 大数据篇 104 1、 写一下 R 的预测脚本,比如 predict.R,是你的主预测的模型; 2、然后用 shell 封装成 xx.sh,比如 predict.sh,shell 里面调用模型,存储数据; predict.sh 的写法如下: # 数据导出 data_filename = xxx file_date = xxx result = xxx updatedt = xxx cd path hive -e "USE tmp_xxxdb;SELECT * FROM db.table1;" > ${data_filname}; # R 脚本预测 Rscript path/predict.R $file_date if [ $? -ne 0 ] then echo "Running RScript Failure" fi # R 预测的结果导入 Hive 表 list1="use tmp_htlbidb; load data local inpath 'path/$result' overwrite into table table2 partition(dt='${updatedt}');" hive -e "$list1" 3. 最后用 Crontab 来进行调度,很简单,如何设置 crontab,度娘一下就好了: >crontab -e ------------------------- ### 每天 5 点进行预测模型; 0 5 * * * sh predict.sh 五、说完了部署上线,说一下模型数据流转的注意事项: 1、区分 offline 和 realtime 数据,不管哪种数据,我们根据 key 和不同的更新频次,把数据 放在 redis 里面去,设置不同的 key 和不同的过期时间; 2、大部分 redis 数据都会存放两个批次的数据,用来预防无法取到最新的数据,则用上一批 次的数据来进行填充; 3、针对 offline 数据,用调度工具做好依赖,每天跑数据,并生成信号文件让 redis 来进行 读取; 4、针对 realtime 数据,我们区分两种类型,一种是历史+实时,比如最近 30 天的累计订单 量,则我们会做两步,第一部分是 D+1 之前的数据,存成 A 表,今天产生的实时数据,存 大数据篇 105 储 B 表,A 和 B 表表结构相同,时效性不同;我们分别把 A 表和 B 表的数据放在 Redis 上 去,然后在 SOA 里面对这两部分数据实时进行计算; 5、模型的输入输出数据进行埋点,进行数据跟踪,一是用来校验数据,二来是用来监控 API 接口的稳定性,一般性我们会用 ES 来进行 log 的查看和性能方面的监控。 6、任何接口都需要有容灾机制,如果接口超时,前端需要进行容灾,立即放弃接口调用数 据,返回一个默认安全的数值,这点对于工程上非常重要。 以上就是我们在模型部署的经验分享,欢迎大家一起来探讨相关工程上的最佳实践。 大数据篇 106 携程机票前台埋点二三事 [作者简介]李宁,携程机票资深数据&产品经理,对于用户行为分析、用户画像、ABTest 等 积累了丰富的经验。 一名数据人如果连埋点和指标模棱两可,则根基不稳,随口一反问都可能成为定时炸弹,坍 塌整个分析过程。如果你认为埋点只是开发的问题,数据人是拿现成的数据来写 sql、完成 分析,未来路可能会越走越窄。 我的理解,数据分析师,可以根据埋点的质量来决定怎么使用埋点,在什么情况下用什么埋 点数据会更贴近事实,很自信地说“我给出的数据是现阶段最可靠的”,面对别人的质疑时, 你的数据无可辩驳。 数据分析师不是抱怨埋点质量差而影响了自己分析,而是应该想“我如何用好现有的埋点来 找到最贴近事实的数据来支持我的结论,埋点质量在不断改进,但我不会等埋点。永远敢于 给出结论。” 携程机票埋点随着业务复杂度的增加而在做加法,先后上的埋点包括 ctm、action、trace、 pv、服务端埋点等五个大类,每个埋点均符合其时代属性,但现在规整起来其相互间存在一 定的交叉,即使冗余但有些埋点一部分还存在价值,转移起来造成的数据问题谁都不想背锅, 所以埋点一直在做加法。直至在 app 减 size 的大趋势下,才顺便把无效的埋点做部分清理。 接下来介绍的埋点在携程机票有其存在的意义,但并不代表是全局最优,如果刚开始埋点的 童鞋,可以参考下面各埋点的优劣势,结合自身的需要来去其糟粕取其精华。 一、UBT 是什么? 全称 User Behavior Tracking system,是由携程首席科学家叶亚明(Eric Ye,携程前 CTO) 发起的一套数据框架,最早从 online 的埋点落入和上传机制体系开始,逐步扩展至无线端 app/hybrid/h5,后又增加 abtest 体系,现在支持携程的众多分析项目。包括数据埋点的格 式、上送契约、落库、ETL 以及最后的报表数据,是数据体系的总称,本文主要对于 UBT 体 系在携程机票的埋点应用以及指标应用做说明。 二、客户端埋点 客户端埋点:ctm,action,trace,pv 1、ctm 埋点 大数据篇 107 玩过 GA(Google Analytics)的童鞋对 utm 埋点肯定不陌生,它以 get 方式记录页面来源,被 广泛使用营销活动的收益结算,Ctrip 的 utm 即为 ctm,主要用于 online 和 h5 平台,不仅 对落地页面的 uv 评估,同时需要根据规则计算其转化。因 ctm 只向后传递一次,未能直接 关联创单(或者 hybrid 页面带到 native 页面面临中断)。 以机票特价页面为例,通常会根据同一天访问过特价首页的 vid、sid 来关联同一 session 的 下单行为,且下单时间在页面访问时间之后,记为间接订单;在此基础上再限制访问特价页 面的出发到达城市(从 url 截取)与订单对应,并限制下单前至最近一次访问特价页面之间未 再次到访过首页(排除看完特价页面后从首页主流程正常下单的情况),记为直接订单。间 接订单的含义是计算特价页面影响的用户下单意愿的程度,而直接订单是计算从特价页面而 下单情况。正常情况下都是以直接订单为主要指标,间接订单作为参考指标。 2、pv 埋点 PV 埋点因存在时间最久、埋点方式最简单(调用 logpage 方法发送 pageid),所以被接受 程度最高,同时也作为新上埋点验证丢失率的基石数据。 app 页 面 实 现 方 式 有 native/hybrid/rn,其均申请独立 pageid,对于计算页面性能影响不大(除了停留时间)。 报表合作机制:作为基础数据,新页面上线第二天就需要在基础报表 UIP 中查到(BI 域数据 T+1)。数据流为,新页面在上线之前开发童鞋会在公司的资源平台 cms 申请 pageid,在页 面加载的时候调用 logpage 接口上传 pageid,行为数据经 ETL 落入 hive 库,经过清洗(去 爬虫、去测试账号等),统计结果数据进入 sqlserver,基础报表平台读取数据做汇总展示。 其中筛选字段可以通过 channelid 来将机票页面区分。整个流程数据流不需要人工干预,完 整的流程保证最小的人力和最快的效率。 页面基础指标:在数据报表中每个页面维度会有 UV、visits、PV、退出次数、页面停留时间。 visits 代表 session/会话数,退出次数具体释义请参考百度。页面停留时间,是该页面与下一 面的 starttime 之差,一般取中位数(屏蔽异常值)。 业务影响 uv:携程机票首页是区分国际和国内的(pageid 不同),如果用户进入时是”北京 “->"上海",则为国内机票的首页,切换到达城市为”墨尔本“时候,则为国际机票的首页。 如果用户上一次是购买的国内商务出行的机票,本次想搜出国玩的机票,进来被默认是记忆 上一次的国内机票首页,这样国内首页的 uv 将被多计算一遍,计算结果将高于实际数据, 所以在计算机票主流程转化率的时候,都是从列表页作为起点。 大数据篇 108 3、action 点击/埋点 点击埋点平台区别较大,按照 native、hybrid、online 顺序说明。 1)native 埋点 埋点格式:为 c_****,比如搜索页面是 c_search,c 代表 click,后面为名称的英文简称,有开发 人员自行定义。点击埋点的 hive 表内有 pageid 字段,命名时只要保证同一页面无重复名称 即可。 埋点流程:app 的 native 默认是有点击按钮即埋点,除非是 pm 特殊指定埋点附加信息(比 如列表页记录筛选 N 次,但不记录筛选内容,如果需要记录筛选内容,需 PM 在 PRD 中说 明)。因为 native 发布周期平均一个月,在之前曾遇到过重大问题没有及时解决因其领导高 度重视,故决定今后所有点击一律埋点,逐步形成习惯。 报表数据流:这个报表暂时还没有上公司的 cms 系统,现在前台产品在维护其埋点简称和 中文名称,由 bi 来调用,生成每天 T+1 的报表。后期考虑维护进入 cms 系统,像 pv 表一 样进入全自动流程 报表字段:点击的报表是计算点击量(PV)、点击用户量(UV)、页面用户量(页面 UV),点 击 uv 比(点击 UV/页面 UV),人均点击次数(点击 pv/点击 UV)。虽然指标很简单呢,但是 如果是跨 BU 或跨公司来核对数据,需要对比计算标准,曾经在和友商核对数据的过程之红, 因对方是服务端埋点、根据服务请求来计算 pv;我方是客户端埋点、根据客户端页面刷新 计算 pv,导致人均 pv 数据明显对不上,后来经过 face to face 的沟通才发现虽然同样说 pv, 但计算方式明显不同。 2)hybrid 埋点 起源:hybrid 埋点在 2016 年 9 月份以前并没有统一的埋点格式,不同的业务开发团队采用 的 js 不完全相同,经过多方 push 统一一套,因 speed 处是点击名称,又俗称“speed”埋 点。 报表数据流:speed 埋点因均为中文,所以不需要人工维护维表,bi 对结果表进行 distinct 即可生成点击筛选框。但这需要开发在 speed 处不可添加变量字段,否则下拉列表将会是一 个灾难。 解决的业务问题:speed 埋点包含订单信息,以hybrid订单详情页为例,我们可以通过orderid 的信息将用户在订单详情页的行为和来电行为关联起来,如果用户在订单详情页上点击“退 票”操作后当天来电“退票”,说明该按钮没有完全解决客户问题,可以在这个点上深挖需 求,改进体验。在快速迭代页面的过程中,关注每个功能的点击后来电的比例,来深究每个 页面细节,,对于快速迭代、精细化数据运营非常有帮助。 大数据篇 109 面对的挑战:因每次上传内容较多,包含系统自带信息,比如设备型号、user-agent 和报错 埋点信息等,导致用户流量消耗较大,待逐步改进。 3)online 埋点 online 埋点采用比较节省流量的方式,即在页面离开(包括进入下一个页面和当前页面刷新) 的时候,将页面上所有的点击信息以{点击名称:点击数量}的 json 格式发送,这样可以节省流 量,但是对于 orderid 等的记录就会缺失,如果增加额外信息需要改变结构,有利有弊。 4、trace 埋点 bi 分析人员希望每个埋点都可以从开始带到创单,这样计算转化率就会比较方便;但开发认 为每个页面埋点重复劳动、浪费时间和精力,而且有可能会影响页面加载速度。为了解决这 个问题,推出了 trace 埋点,这个埋点的特点是每个主流程页面仅有一个,但将所有的业务 信息记录在案。 埋点格式:每个主流程页面均有 trace 埋点,在页面加载或离开时发送,由 bi 统一管理, app/online/h5 格式基本一致,所有 trace 修改都需要经过 bi 的审核,主流程页面包括首页、 列表页、中间页、填写页、完成页。 作用效果:可以根据业务属性来区分具体人群的行为转化,故又称“业务埋点”。比如在首 页勾选儿童之后,通过这个"children"的标识位可以看到有儿童购票意愿的细分人群在各个 主流程页面间的转化(该标志位只有回到首页重新取消勾选的时候才会刷新这个标识位,否 则都是从首页一直带下去)。这样对于细分人群的体验改进效果具有可观测性。 三、服务端埋点 机票 OTA 承担航司很多政策任务,会在列表及中间页通过标签的形式来给客户不同产品体 验,但这些政策标签能够带来多少销量的提升,以及如何决定其之间的相互影响,成为一个 课题。于是在服务端从列表页开始,将所有的显示报文埋点记录下来。 大数据篇 110 效果:对于所有产品可以根据政策维度和航司维度进行筛选,通过展示转化比来观测各个阶 段的转化,同时对于后台对应的政策业务人员可以发送针对性报表,各取所需,节省大量时 间。后期将利用机器学习方法针对不同政策、价格和排序的相互关系进行测算,希望找到最 优转化的显示方式。 四、指标理解 携程机票的埋点体系基本如上所列,能够清楚明白每种埋点的优劣势对于分析问题选用数据 的时候非常有益。通过埋点反映出来的指标,尤其是二次计算指标,很多在网络上已经有详 细定义和说明,我将就结合携程机票的应用以及复盘过程中的思考做一下说明,希望能有所 启发。 大数据篇 111 1、数据关联 关联需要注意的是,不同的埋点的缺失率是不相同的,以下的关联准则是经过作者在部门实 践中的反复验证所得,不一定具备普适性。 行为和订单的关联,以 app 为例,关联同一人,行为主要是 clientcode 设备号,订单主要是 uid,这两者之间通过临时订单表关联(在填写页创单的时候创建临时单),把 clientcode、 uid、orderid 订单号记录下来(如果拆单的话,仅记录主订单),然后需要通过订单主表 o_orders 来把实际下单数据过滤出来,最后可以拿到 clientcode 在每个 session 中的下单记 录以及 uid 映射。 行为和行为的关联,一般是通过 clientcode,sid,pvid 来定位同一个页面的行为,如果是核心 数据,如订单号建议直接埋点,不建议通过关联拿到。尤其是在小众人群的匹配上,数据的 缺失的基础上进行关联可能会造成数据异常波动。 2、数据缺失 现状:公司的 pv 表的存在时间最久,而且埋点最简单,结构最稳定,所有的验证数据都是 以 pv 表的数据为基准。经过验证下来,根据按天计算的 uv 数据,trace 的埋点准确率在 97% 左右,服务端的埋点在 103%左右。如果都是在同一类埋点的情况下计算转化率,分子分母 是每个页面的 uv,影响不大,但是跨埋点计算的时候,需要特别小心。在数量级明确之后, 还存在数据格式的问题,尤其是 string 和 int 的转化,特殊字符造成的解析困难等,这些都 需要在使用过程中不断验证,bi 和开发相互磨合。 埋点的准确率受很多因素的影响,主要是不畅沟通带来的各方 gap,最后体现在开发对埋点 的重视程度不足。每个开发对于埋点的认识不同,对于埋点上送的逻辑也不尽相同,再加上 心态不同可能导致结果也会差别很大。 1)常见的几个埋点问题: 不该触发的时候而被触发:hybrid 页面曾遇到过只要是手指划过按钮埋点就被触发,导致新 页面上线后点击数据异常暴增,其实是开发在判断触发事件的阈值设置错误,停留时间超过 200ms 以上才算点击,小于 200ms 算滑动,但是在上面那个例子中开发未做限制,导致问 题。 埋点触发相互抑制:在一个新埋点上线后,发现一个毫不相关的点击数据下降明显,从业务 上找不到原因,后来开发查找代码的时候发现,两个埋点的上送逻辑存在 ifelse 关系,只有 一个被上传。 开发与埋点不是同一人导致逻辑异常:这主要存在于开发交接时候对于埋点的上送逻辑一般 不太重视,所以在业务发生变化的时候,并没有及时更改埋点的逻辑,比如 pm 希望某个默 认埋点的默认显示被记录,最早是由服务端直接下发,客户端不做筛选,所以客户端买点直 接读取服务端下发内容,但一段时间后默认逻辑在客户端加一层个性化接口,埋点方式还是 直接读取服务端内容,未做更改,导致数据一直异常,经过好长时间的努力才定位问题。 大数据篇 112 部门开发和框架之间的冲突:有时候部门开发逻辑做的很完整,但是被框架的一些逻辑所限 制,被背黑锅。比如为了优化速度,hybrid 页面在本地 app 打包的过程中有些文件已经放 入,在 hybrid 请求的时候,有些文件优先以本地为主,而公共框架部门做了一些拦截,但业 务的开发可能就存在没考虑到这层逻辑,埋点数据就会全部丢失。 2)开发对埋点的误解 问:为什么每个页面都要埋这么多点,难道不能通过关联来实现吗? 答:在开发本身的任务都很重的情况下,埋点相对次要,在不了解其意义的情况下,往往意 愿不强,怨声载道。这就需要 pm 或者 bi 很清楚地知道哪些埋点数据一定要有,哪些是可 有可无,同时在整个项目上的最终数据表现上跟开发童鞋分享数据,强化埋点的价值。另外 对于开发童鞋本身比较关注的 kpi,如页面性能埋点,包括报错信息、加载时间、白屏等, 可以辅助其建立报表来增强对数据的关注度。 问:为什么埋点动不动就要增加,能不能一次性提好? 答:这是个历史性的难题,因为在分析问题的时候,维度在不断地细致化,而这些维度是在 当初并没有想到的、或者说可能认为没有必要的埋点(没有必要的埋点不增加开发的工作 量),但是问题发生之后就需要增加埋点,这也是需要与开发保持密切的沟通。 3、新老用户 定义:从访问维度上看,该设备号历史上从未访问过携程 app,则该设备为访问维度上的新 用户;从订单维度上看,该 uiv 历史上从未在携程 app 上成功下单,则该设备为订单维度上 的新用户。 uv 的区别:从访问维度上看,是通过设备号 vid/clientcode 来看;从订单维度,是通过 uid 来看。 设备平台的区别:即使该 uid 在机票 online 上已下单,某天在 app 上第一次下单,则也被 认定为 app 的下单新用户。 辩证关系:如果一个人是 app 平台的下单新用户,则该设备号一般为访问新用户(一般很少 有人把自己手机借给别人登陆携程账号,因为如果是帮朋友代订可以用自己账号下单,如果 有的话成为异常用户的概率比较高);一个设备号被认定为某天的 app 新用户,则该 uid 不 一定是下单新用户(因为不一定下单,且有可能是该 uid 买了新手机。)携程机票是相对成 熟的 app,新老用户的比例基本保持动态平衡。 4、留存率/回购回访率 回购率:季度回购率,机票是低频消费产品,回购率的比率经过长期观察发现季度的周期比 较有指导意义。 大数据篇 113 回访率:月度回访率。 5、停留时间 定义:该页面与 pvid+1 下一页面的 starttime 之差,计算方式一般采用中位数(规避异常值 影响整体表现)。 session 时长计算:首次搜索->下单时间、末次搜索->下单时间,反映用户决策时长的两个 指标,计算方式为同一 session。但机票的购买决策时间比较长,从起意到最后下单在一个 session 完成的比例比较低,未来考虑在跨 session 的情况下计算其时间,尽量接近真实的停 留时间。 native 和 hybrid 混用的停留时间之殇:停留时长的计算是利用 pvid+1 的页面与本页面的访 问时间差来计算的(艾瑞在 online 端的访问时间是 duration,表示激活时间,能够实际表示 当前页面的停留时间),而如果 native 和内嵌 hybrid(已申请 pageid)先后加载的时候,填 写页的停留时间其实就变成 hybrid 页面 starttime-native 页面的 starttime,这中间的时间差 其实是两页面加载的时间差,并不是用户真实的停留时间。 停留时间是否越短越好? 对于携程机票的电商网站来讲,停留时间是一个辅助的指标,而非一个决定性的指标,需要 和一些决定性的指标一起来推测用户的行为。 比如同样是填写页的停留时间变短,在填写页之后的转化率上升的情况下,可以理解为该页 面让用户非常放心,用户需要填写和核对的信息很少,对携程的网站非常熟悉和自信,下单 迅速,这是一件正向的事情; 而如果是填写页之后的转化率下降,就有可能是页面冗余信息很多,用户想关注的信息没有 找到,或者造成用户反感的信息非常醒目,导致用户立即离开而没有下单,这就成为一件棘 手的事情。结合业务可能会找到很多原因,但有一点可以肯定,单纯追求停留时间的上升或 者下降是没有意义的,TA 需要核心指标一起来定位原因。 6、行为流可视化的必要性 可以建立每个用户的行为流表,方便 pm 根据 uid、手机号等常用字段可以搜索到用户的页 面和点击行为流,方便查找问题以及找到问题解决的灵感。因为解决问题是从特殊到一般的 过程,可以通过行为流找到灵感,然后用 sql 来验证是否具有普适性,分析能力螺旋式上升 的过程。 在埋点新上线测试环境进行对比,实际看到埋点的数据格式是否符合预期。 大数据篇 114 五、总结 携程市值从 2014 年的 60 亿到 2016 年的 140 亿美金,其中 2016 年机票的贡献利润超过 40%,快速发展的业务必然需要大量的数据作支撑来推进整体向前发展,而发展的问题需要 通过发展来解决。整体埋点体系其实比较复杂,其中的困难也不必多说。在整体趋势下,我 一直秉承两个原则: 1、用户产生交互的埋点一般放在前端,因为客户端离用户最近,且有些逻辑是放在客户端, 点击后不一定都会发送服务;而服务端是以展示为主,可以记录整个下发的报文数据,详细 分析显示转化比。 2、概念非常复杂时,将相似的概念放在一起对比理解,防止概念混淆,比如退出率和跳出 率;UV 数/会话数/PV 数;回购率和回访率等。 【附录】 vid/clientcode/clientid:设备标识字段,vid 应用于 online 和 h5(受浏览器和 cookie 限制, 换浏览器或清除 cookie 之后会更新 vid), clientcode 代表 app/native 和 hybrid(仅与设备 有关,在不换设备的情况下标识唯一) sid:sessionid,又称为会话 id,以 online 为例,同一设备访问同一网站 30min 之内为同一 session。 pvid:pv 的计数,同一用户在一天之内的携程页面访问 pv 从 1 开始标识,记录该人当天内 的所有访问页面的先后顺序,再根据 30min 来切 session。 starttime:事件发生时间,在 pv 表指页面浏览时间,在 action 表指按钮触发时间。 UV:在考虑行为的时候都用设备号来标识,在考虑订单的额时候都用 uid 来标识。 大数据篇 115 携程机票的 ABTest 实践 [作者简介]李宁,携程机票资深数据&产品经理,对于用户行为分析、用户画像、ABTest 等 积累了丰富的经验。 携程 ABTest 伴随 UBT(User Behavior Tracking System)系统一起,两年多的时间,从最初 online 寥寥几个实验,到现在单是机票 BU 每周就有数十个 app/online/h5 平台同时线上运行。 在 14 年-16 年 James(编者注:携程创始人/董事会主席)领导下的技术驱动的大携程体系, 对于项目上线的收益、年终 KPI 考核、CEO 奖项的评比都需要拿 ABTest 数据说话(据说 James 对数据特别敏感,曾经在 2016 年某 hackathon 决赛中,未上线前一 team 在台上大 谈项目收益,James 当即打断说“你们的项目上 AB 了吗?没上的话不要讲了”,该 team 被当场淘汰) 。 携程市值不断增长的背后,是无数个 ABTest 的支持,而携程机票大部分的 ABTest 都放到前 台来配置,有幸在 2016 年经历 N 多大项目的上线 ABT 过程,本文将以此为背景来说明携 程机票对于 ABTest 的应用。(ABTest 在携程被简称为 ABT) 一、ABTest 的定义 ABTest 本身其实是物理学的“控制变量法”,通过只改变一个因素来确定其变化对 CR(conversion rate)或者收益的影响。其本身具备统计意义,而且具备实际意义。 试想一下如果没有 ABTest,那新项目上线后的收益如何排除季节因素、市场环境因素的影 响,而且一个页面上如果同时做多处改动,如何评判是哪个改动造成的收益或损失?这对一 个理性思维的人是不可接受的。 简单理解为将一群人分成两类,通过展示新旧版 version A/B 来测试哪种版本效果好,差异 是多少。 大数据篇 116 二、ABTest 流程 ABTest 数据流:APP 启动时,公共框架会拉取所有线上 ABTest 的试验号和对应版本(所有 BU)存入本地,当用户进入机票频道时候,在特定场景触发本地实验号调用。比如往返实 验,在用户首页点击往返搜索时,开发会从本地文件中查询 160519_fld_round 试验号该用 户的对应版本,确定跳转新/旧版页面。在试验号接收到调用时,同时触发一个 ABTest 的 trace 埋点 o_abtest_expresult,该埋点会记录 clientcode,sid,pvid,试验号及版本信息,最 终经过 ETL,BI 会汇总一张 AB 实验表,将上述信息汇总,便于后续做关联计算。 分流计算:每个设备在刚启动的时候会根据设备号+试验号+随机数组成一串 N 位数,对 100 取模的余数从 0-99,假设 ABCD 四个版本流量 10:70:10:10 的情况下,则余数 0-9 为 A 版、 10-79 为 B 版、80-89 为 C 版、90-99 为 D 版。A 版为默认版,如果尾数异常(Null 或溢 出),则走 A 版。 AB 版本:如果仅有新旧两个版本的情况下,一般会设置 ABCD 四个版本,(其中 ACD 为旧 版,B 为新版;如果有多个迭代新版,则从 EFG 开始)来进行 AA 测试和 AB 测试。 1、AA 测试:CD 版同为旧版,且流量各为 B 版一半,在流量随机分配的情况,通过对比 CD 大数据篇 117 版的数据表现来验证旧版的状态是稳定的。A 版作为旧版,也称为兜底版本,BCD 的剩余流 量走 A 版,版本异常的情况下走 A 版。 2、AB 测试:在确保 CD 数据相对稳定的前提下,再对比 B 和 ACD 版本的数据,来对比新 旧版的差异。 实验正交性: 1、非正交实验:如左图展示,在旧版的基础上再做区分,会因为样本数量的问题而限制同 时进行的实验个数,而且无法评估两个新版同时存在的影响。 2、正交试验:右图展示,不同实验流量完全打散随机分配,上一个实验与下一个实验理论 上流量上没有关联,这样可以在一个页面同时进行多项实验。 这里再提一个 Magic Number = 7,虽然理论上单页面上同时进行的正交试验数量没有上线, 但是经过长期经验积累,单页面同时线上实验不要超过 7,否则会造成难以捉摸的幺蛾子异 常。 埋点下线机制: 像 ABTest 里面的埋点触发场景埋点还是由开发控制的,也还是会存在埋点不准确的情况, 比如说往返的实验,触发场景是在首页点击往返搜索,理论上去程列表页的 UV 应该是参与 式样的样本数抑制。实际情况是,去程列表页 30W 的 UV,但 ABT 的报表显示每天样本为 50W,经过 SQL 验算两者交集为 20W,就说明有 10W 人是在往返流程但并没有参与实验(数 据经过脱敏处理,但不改变相对位置)。 所以基于这样的幺蛾子,在 ABT 结束后,既要删除代码,又要实验流量全开 100% 1、流量调整 100%目的:将历史版本的客户端旧版规避,需要操作 100%流量。 2、下线代码:保证 app 的 size 不会过度冗余,同时因埋点场景的问题,有些时候虽然流量 全切 100%,但仍有部分流量走旧版(非常诡异),所以将客户端代码下掉是非常必要。 其他说明: 大数据篇 118 1、在任何情况下,分析的基础条件就是流量随机分配,如果质疑这件事情,则整个 ABTest 就失去意义. 2、实验分流一般采用设备号 clientcode,但是也可以根据 uid 来,但情况较少 3、对于实验的显著性指标 P 值一般使用较少且不易理解,就不做过多解释(一年也没怎么 用这个指标) 4、分流调节机制,新版流量不要忽上忽下,特别是涉及到核心页面的时候,否则可能会造 成用户看到的页面反复变化,增加适应时间和学习成本以及影响用户体验。 三、分析数据 ABT 的目的:ABTest 是希望通过如何改进新版优于旧版,而不是通过 ABTest 证明新版弱于 旧版而下线实验,所以需要有效的分析数据。 如何看图表: 图表反映时间趋势,在 ABTest 中表现为新旧版本两条折线图,且一般会出现交叉的情况, 那我们就需要判断这些交叉是有随机性波动还是实验的效果,我在实践中总结简单易用的一 条原则是:“抓大放小”。 抓大放小(个别表现不影响整体趋势):当你遮住有限个点的时候,不影响整体的差异。比 如下图,当你遮住 2-11 和 2-13 两天的数据时,会发现蓝色 B 版优于红色旧版。(当然遮住 点的数量因人而异,一般不超过总量 10%) 这张图就很难用抓大放小的方式来判断差异,无法证明是新版好还是旧版好,这时候需要分 解这个指标来继续分析。 大数据篇 119 机票的核心指标是转化率 CR(conversion rate)和收益(revenue),通常他们之间的关系如下图 所示。 携程机票前台以 scrum team 的形式迭代,每个 team 对于需求的评审是以 ROI(投入产出 比,return on investment)来决定项目的优先级,而 return 可能是 CR 的提升,也可能是单 票收入的提升。 对于上线实验数据跟踪,也是以当初 ROI 的预期来进行判断实验效果,尤其在没有达到预期 的情况下,寻求解决方案。(这其中还有诸多限制条件,比如收益类项目,如果 CR 有明显下 降需要重点关注。) 分析思路 大数据篇 120 这个分解公示也代表分析的思路,无论对于收益类项目还是 CR 类项目,都会先看单 UV 收 入和 CR(一般情况下,ABTest 不会改变每个订单的票量,这是基于整体订单估算的平均值, 我们暂且认为 TA 是常量),当这两项都保持正向增长的情况下,那可以直接开大流量继续 验证直至项目完美收官(这种案例比较少)。更多的情况是,对于重大项目,即使结果是积 极正向的实验,我们也会大概了解下改进点发生在哪个页面或者哪个产品,做到心中有数; 而当发生问题的时候,我们都会对 CR 和单票收入做分解: 1、CR 下降的情况,看主流程每个页面的 CR,是哪个页面下降,从页面的来源去向和点击 来看,是否有明显的异常,一般来讲,对业务足够熟悉的 PM 在这一步可以结合业务和这些 数据大概会有一些预判,是哪些因素可能造成的影响,之后再请教 BI 专业人员或者自己拉 SQL 来验证数据,从而进行改进。 2、利润下降的原因,继续分解指标,可以分产品、航司、利润构成等指标来分解,找到新 旧版的 gap,然后结合业务场景做一些预判,进行找数据来支持这个想法,继续迭代新版。 之前的状态是 PM 对于 AB 实验的数据有一大坨报表,但是并不知道如何使用,也不知道怎 么看报表,不知道怎么分解指标,但其实对于整体进行了解之后,具备简单的分析能力,关 键是有业务背景知识的情况下,这样的几个公式的八股文的分析可以解决 80%的问题,对于 实在无法定位的问题,可以找 BI 寻求帮助。 四、总结 ABTest 其实核心在于如何定位问题解决问题,但是限于身份不能通过数据来进行举例说明。 但其实分析思路应该是一致,比如机票场景下指标分解的核心公式来解决 80%的问题,在每 个行业应该都会有这样的公式,可以根据特定业务背景自己总结运用。 PM 如能够掌握这些基本的指标分析、能够看懂图表、这里面就能够自助解决 80%的问题, 这样的 ABTest 效率其实已经是非常高的。 大数据篇 121 AAAI-2017 见闻 | 那些最牛逼的公司都在研究什么 [作者简介]吴忠伙,携程技术中心基础业务研发部资深算法工程师。2013 年加入携程,目前 专注于个性化推荐,自然语言处理等领域在旅游行业的应用研究和实践落地。 AAAI-2017 (the Thirty-First AAAI Conference on Artificial Intelligence )今年 2 月 4 日至 9 日 在美国旧金山举办。在会议进行几天里,因恰逢雨季(差不多从 11 月至第二年的 3 月),旧 金山几乎天天在下雨。忙里偷闲去的金门大桥,也被雨水和雾气笼罩着,见图 1。 图 1 金门大桥 AAAI成立于1979年,原名美国人工智能协会(American Association for Artificial Intelligence), 2007 年正式更名为人工智能促进协会 (Association for the Advancement of Artificial Intelligence)。目前,AAAI 是全球人工智能领域顶级的学术会议,人工智能领域研究的风向 标,在全球有超过 230+的 Fellows,4000+的 Members。 AAAI 2017 会议包含了很多项目,有 Tutorials、Invited talk、panels、AI in practice、Technical talk、poster/demo session、Awards Ceremony、workshop、Doctoral Consortium、Opening Reception 等等. 安排的十分紧凑,只能有选择性的参加了一些项目,给大家分享一下。 AAAI 这次在湾区举办,充分利用地理优势,专门设立了“AI in Practice”项目。邀请了 Google、 Facebook、Amazon 、LinkedIn、Uber、IBM、Quora、百度,八家科技公司人工智能负责人 来分享各自领域的技术和研究进展。 Google Brain Team 的首席科学家 Vincent Vanhoucke 分别介绍了他们团队在图像、语音、 机器翻译方面的研究成果,以及机器人方面的工作,他讲述了机器人领域很多值得深究的点, 大数据篇 122 比如如何协调感知和执行,如何提高样本的有效使用等等。 Amazon 的机器学习总监 Alex Smola 做了 Scalable and personal deep learning with MXNet 主题分享,大谈深度学习框架,以及好的深度学习框架需要满足的条件,并且拿 MXNet 与 Caffe、TensorFlow 等做了比较。 Facebook 的 Applied Machine Learning 负责人 Joaquin Quinonero Candela 做了 Designing AI at Scale to Power Everyday Life 主题演讲,他们开发的学习系统可以快速应用于图像、文 本、语音等方面任务,其还拿图像识别平台 Lumos 举了一些案例,直观的展示了功能和算 法的强大。 大会安排了很多场的 Invited Talk/keynote,其中有一场是来自 MIT Media Lab 的大牛人、 “情感计算”之母 Picard 教授,题目是 Adventures in Building Emotion Intelligence Technologies。她主要介绍了为什么要研究“情绪计算”,“情绪计算”的由来,提到了一 些用于情绪计算的机器学习算法效果。在演讲中还展示了:实时面部表情情绪识别 APP- AffdexMe;通过智能手表监测紧张与焦虑;使用智能手机监测心率、呼吸频率等;还有监测 大脑活动、癫痫猝死症等等方面的成果。而她的团队最近在研究如何应用 AI 技术预防抑郁 症,这是非常有意义的事情。 图 2 视频图像情绪识别 大数据篇 123 图 3 视频图像情绪识别 论文 Oral/Poster 分享宣读部分,每个时段大概同时 8-9 个 session 在 8-9 个场地同时进行, 大家各自选择关注的 session、关注的论文进行听讲。当天晚上会有白天宣读过的论文,进 行 poster,对你的论文工作感兴趣的人会找到你摊子跟你一起进行深度交流。观察下来,最 火的 session 还是跟深度学习应用研究相关,会场不仅坐满人,还站着一大批。关于 AI 系统 部署研究关注的人也比较多。 今年的杰出论文奖是颁发给 Stanford 的 Russell Stewart 和 Stefano Ermon,他们提出一个开 创性的研究工作成果,Label-Free Supervision of Neural Networks with Physics and Domain Knowledge 的灵感来自人类的学习过程,文章提到一种新的神经网络监督学习,不需要给出 输入-输出这种标记直接方案,可以通过满足特定约束条件和物理定律来指定输出空间。论 文展示了不带任何直接标签的情况下,通过约束学习做了三个案例实验,方法成功训练了 CNN 用来检测和跟踪对象,效果还是不错。 另外也有很多其它有趣的论文,例如图 5 来自 google 的 Inception-V4,Inception-ResNet and the Impactof Residual Connections on Training(结合了 Residual Connection, 发现极大 加速训练,提升性能,另外还提出了一个更深更优化的模型 Inception-V4),大家有兴趣可以 去翻看会议的论文集。 大数据篇 124 另外,会议还有多个 Panel, 分别讨论了 AI Ethics Education、AI for Education、AI for Social Good、AI History: Expert Systems 和 Advances in AI in Poker: Two Mini-Talks on Recent Breakthroughs and a Panel。我们参与了 AI for Social Good 这个 panel,大家都对 AI 在帮助 社会公益有非常多的想法,无论是讲台上几位教授,还是台下的听众,讨论持续了很久,挺 有意思。 携程是第一次站在这样一个国际顶级学术会议的舞台上展示自己的研究成果。文章 A Hybrid Collaborative Filtering Model with Deep Structure for Recommender Systems,是基于公司自 主研发的通用化推荐系统而写成。 论文提出了一种 Additional Stacked Denoising Autoencoder (aSDAE)的深度模型用来学习 User 及 Item 的隐向量,该模型的输入为用户或物品的评分值列表。每个隐层都会接受其相 大数据篇 125 应的 Side information 信息的输入(该模型灵感来自于 NLP 中的 Sequence-to-Sequence 模 型,每层都会接受一个输入,模型中每层接受的输入都是一样,因此最终的输出也尽可能的 与输入相等),模型见图 8。 图 8 Additional Stacked Denoising Autoencoder(aSDAE) 结合 aSDAE 与矩阵分解模型,提出了一种混合协同过滤模型,见图 9。该模型通过两个 aSDAE 学习 User 与 Item 的隐向量,并利用学习到的隐向量的内积去拟合原始评分矩阵 R 中存在的 值,其目标函数由矩阵分解以及两个 aSDAE 的损失函数组成,最终通过 SGD 优化目标函数 学习得到 User 和 Item 隐向量。在多个数据集上做了对比实验,结果显示混合协同过滤模型 在比已有方案性能都要表现的好,具体可以参考 paper。 大数据篇 126 图 9 混合协同过滤模型 OTA 与传统电商网站不同,用户低频使用、低频消费导致数据稀疏性问题很严重。论文主要 解决数据稀疏性问题,目前论文成果已经在 50 余个通用化推荐系统支持的个性化场景中使 用,个别场景转化率提升了 13 倍之多,有效提升了用户的出行体验,为公司带来很大的经 济效益。 未来,除了个性化推荐领域,我们还会在自然语言处理、图像、语音等方向做深入的实践和 研究。相信不久的将来,携程会有更多的研究成果出现在人工智能相关顶级会议上,分享给 大家。 大数据篇 127 从底层到应用,那些数据人的必备技能 [作者简介]潘鹏举,携程酒店研发部 BI 经理,负责酒店服务相关的业务建模工作,主要方 向是用机器学习帮助业务创造价值。 前言: 谨以此文献给对数据有热情,想长期从事此行业的年轻人,希望对你们有所启发,并快速调 整思路和方向,让自己的职业生涯有更好的发展。 根据数据应用的不同阶段,我将从数据底层到最后应用,来谈谈那些数据人的必备技能。 1、大数据平台 目前很火,数据源头,各种炫酷新技术,搭建 Hadoop、Hive、Spark、Kylin、Druid、Beam~, 前提是你要懂 Java,很多平台都是用 Java 开发的。 目前很多企业都把数据采集下来了,对于传统的业务数据,用传统的数据是完全够用的,可 是对于用户行为和点击行为这些数据或者很多非结构化的数据,文本、图像和文本类的,由 于数据量太大,很多公司都不知道怎么进行存储。 这里面要解决的是实时、近实时和离线的大数据框架如何搭建,各数据流之间如何耦合和解 耦,如何进行容灾、平台稳定、可用是需要重点考虑的。 我的感觉是:最近两三年中,这块人才还是很稀缺的,因为大数据概念炒作的这么厉害,很 多企业都被忽悠说,我们也来开始进入大数据行业吧。进入的前提之一就是需要把数据存储 下来,特别是很多用户行为方面的数据,对于业务的提升比较明显的,如果你能很好的刻画 用户,那么对你的产品设计、市场营销、开发市场都是有帮助的。现阶段,很多公司都要做 第一步:存储更多的数据。这也是这块人员流动性比较高的原因,都被高薪挖走了。 和传统的 SQL 不同的是,针对大数据量的非结构式数据,我们所想的就是:用最廉价的成 本存储数据同时能够达到容灾、扩展性高、高性能、跨域,从目前来看,分布式已经被证明 是个很好的一个方式。 另外,云端会是个很好的方向,不是每个公司都养得起这么多这么贵的大数据平台开发人员 和运维人员 OPS,从事这个行业的我们要有很好的危机意识,及时贡献出自己的价值,积极 主动的学习新技术、否则就可能被淘汰了。 此外,花点钱把数据托管给云服务提供商是对于创业公司或者一些传统的企业来说是个很好 的思路,这样能够最快速的确定数据对你的价值是什么,而不用采购这么多的服务器、雇佣 这么多的运维人员和网站开发人员。 大数据篇 128 说了以上这些,主要是想给未来会从事这块的人或者想存储数据的公司一点方向。我自己不 做这块,体会不深,大家看看就行。 这块工作最被吐槽的一点就是:Hive 速度好慢,SQL 查询好慢,集群怎么又挂掉了,hadoop 版本升级后,怎么数据跑出来不对了等等。 因此,在这个领域内工作,需要有强大的攻坚能力,并且还需要有快速定位和解决 bug 的 能力,因为有很多工具都是开源的。因为是开源的,所以你们懂得,各种坑爹,甚至出现无 法向下兼容的情况,所以需要强大的 Java 开发能力。 如果想在这块做的很好,还需要有整个系统架构的设计能力、比较的强的抗压能力和解决问 题的能力、资源收集的能力,可以打入开源社区,这样就可以随时 follow 最新的潮流和技 术。 2、数据仓库-ETL 确实做仓库的人很辛苦,单单 Oncall 就会让人望而却步。有很多数据库工程师,晚上睡觉 的时候经常被 Oncall 电话吵醒,因为数据流程出问题,需要第一时间去排查,是哪个数据 源出问题,并且要立即解决,否则整个数据流程都会受到影响。 如果数据流程受到了影响,你就可能会被大领导一言不合叫到办公室说:我要的数据怎么还 没有准备好,我的业务报表今天怎么没有发出来。 通过上面这个情景,我们可以知道:这是个很重要的岗位,因为数据流程很重要,决定了数 据从源头杂乱无章的状况,通过 ETL 之后变成了整齐的数据,这些整齐一致性的数据可以让 你很方便地把各业务的统计结果计算出来,并且能够统一口径。要不然就会变成有几个部门, 就有几种统计结果,到时候 A 部门说业务增长了 5%,B 部门说业务涨了 10%,OMG,到底 信谁。 至少在以下几点上,我觉得数据仓库人员应该要做好: a、数据字典的完整性,用的人都希望能够清晰的知道这个字段的逻辑是什么。字段要保持 很好的一致性,不要同样一个字段在不同表里有不同的定义。 b、核心流程的稳定性,不要让每天订单主表能够使用的时间很不稳定,有的时候很早,有 的时候要中午才出来,如果不稳定就会导致使用数据的人对你很没有信心。 c、仓库版本迭代不要过于频繁,要保持不同版本之间的兼容性。不要做好了仓库 1.0,很快 就把原来的推倒重来,变成了 2.0。在数据仓库中需要考虑到延续性,主表的变动不要太频 繁,否则使用的人会非常痛苦,好不容易才用习惯了 1.0 的表结构,没办法这么快进行切换。 简单地说,要能向下兼容。 d、保持各业务逻辑的统一性,不要出现同样的业务逻辑,同一个组别的人统计出来的结果 不同。原因在于共同的逻辑没有落地成通用的东西,所以导致每个人写法不同。这点其实需 大数据篇 129 要特别注意。 针对以上,这个岗位的技能要求是:不要成为仅仅会写 SQL 的人,现在工具都很发达,如 果你的技能很单一的话,那么可替代指数是非常高的,并且你自身也没有什么成就感。这里 并不是说会写 SQL 的人很 low,只是说应该多学一些技能,否则会很危险。 仓库人员应该要常常思考,如何进行架构设计是最合理的,你要考虑是否需要字段冗余、行 存储还是列存储、字段如何扩展最有效,热数据和冷数据如何拆分等,所以需要有架构思维。 技能上,除了 SQL 熟练之外,还需要知道如何写 Transform,MapReduce,因为有很多业务 逻辑用 SQL 实现起来非常复杂,但是如果你会其他脚本语言,那么就能给你提供便利,让 你的效率提升很多。另外好的仓库人员需要写 Java 或者 Scala,通过写 UDTF 或者 UDAF 来 提升你的效率是很有必要的。 数据仓库人员也应该常常考虑自动化和工具化方面的事情,需要很好的工具或者模块的抽象 能力,动手实现自动化的工具来提高整个组织效能。针对经常碰到的数据倾斜问题,需要很 快定位问题并进行优化。 说完了数据存储这块,接下来是数据应用的几个关键职位,在此之前,我想说数据应用的一 个最关键的前提是:数据质量、数据质量、数据质量!!在每次阐述你的观点、分析结论或 者用算法的时候,都需要先检查,源头数据正确性,否则任何结论都是伪命题。 3、数据可视化 这是个很炫的工作,最好是能懂点前端,比如 js。数据可视化人员需要有很好的分析思维, 不能为了炫技而忽视对业务的帮助程度。因为我对这个岗位客串的不多,所以没有特别深入 的感悟,不过我觉得这个岗位需要有分析的能力,才能把可视化做好。 另外一方面来说,做数据应用的人都应该懂点数据可视化,要知道观点表达的素材顺序是: 图片>表格>文字,一个能够用图片来阐述的机会千万别用文字来描述,因为这样更易于让 别人理解。要知道,给大领导讲解事情的时候,需要把大领导设想成是个“数据白痴”,这 样才能把一件事情说的比较生动。 4、数据分析师 现在对数据分析的需求是很大的,因为大家都想着说:数据有了,但是能做些什么呢?这就 需要有数据分析师,对数据进行分析和挖掘,然后做数据应用。 对数据分析师吐槽最多的是:你分析出来的不就是正常的业务逻辑吗,还需要你分析什么? 或者是你分析的结论不对,跟我们的业务逻辑不符合。特别是:ABTest 的结果和当初设定的 预期不相符合的时候,分析师会常常被拉过去说:分析一下,为什么我的 AB 实验结果不显 著,里面肯定有原因的。 很多时候,宝宝的心里苦啊,你说这个转化率下降了,从数据上可以看出哪个细分渠道下降 大数据篇 130 了,至于为什么客户不下单,我们得去用户去,很多时候,数据上也体现不出来为什么,只 能告诉你现状是什么。 如果你一直在写分析报告,给结论中,持续周而复始,没有直接在业务中体现成绩的时候, 数据分析师们该醒醒了,你该想想这个是你要的岗位吗? 对于数据分析师的定位:个人认为,成为优秀的数据分析师是非常难的,现在市面上也没有 多少优秀的分析师。数据分析师的技能要求,除了会数据分析、提炼结论、洞察数据背后的 原因之外,还需要了解业务,懂算法。 只有这样,当面对一个业务问题时,数据分析师们才可以针对问题抽丝剥茧,层层递进去解 决问题,再根据定位的问题进行策略的应对,比如是先做上策略进行测试还是应用算法进行 优化,用算法用在哪个场景上,能不能用算法来解决问题。 一个优秀的数据分析师,是个精通业务和算法的全能数据科学家,不是那个只会听从业务的 需求而进行拉数据、做报表、只做分析的闲杂人等。我们都说分析要给出结论,优秀分析师 的结论就是一个能解决问题的一揽子策略和应对措施,同时很多需求是分析师去主动发现并 通过数据来挖掘出来的。 从上述描述中,可以看到对数据分析师的要求是:会写 sql 拉数据,精通业务、会数据洞察、 精通算法,主动性强,要求还是很高的。 如果你一直只是忙于应付日常分析需求,热衷于写华丽的报告,那么你要记得,你很危险, 因为会有一堆人在那里质疑你存在的价值,特别是小公司。因为数据人员的薪资是个不小的 支出。 大部分不落地的分析都是伪分析,有一些探索性的可行性研究可以不考虑落地,但是其他的 特定业务需求的分析都需要考虑落地,然后通过实践来反推你的作用,如此反复,才能慢慢 的给你价值的肯定,同时提升你的分析技能,也只有这样才能证明你作为分析师、数据落地 者的价值。 5、数据挖掘/算法 这块的话,经过这三年的摸爬滚打,感触蛮多的。体会比较深的吐槽主要有以下几点: 一个规则搞定了,还用什么算法。 你的准确率怎么这么低?! 你的准确率可以到 99%吗? 你的推荐有价值吗?你不推荐客人也会下那个产品的订单的。 帮我做个大数据预测他想要什么? 很多时候,不同的场景对准确率的要求是不同的,所以在一定合理的场景下和业务进行据理 力争是必要,不要害怕让业务吐槽,更多的时候管理好他们的预期。 大数据篇 131 有些场景下,推荐的价值在于『长期复购率』,所以不要每次都盯着 ABTest 的转化率来说事, 让客人的费力度降低也是很有前途和前景的。一个智能的产品会让客人用起来爱不释手,虽 然在这一次的转化中没有明显的差别,但是观察长期复购率才能体现价值。特别是要区分: 高频和低频产品。频次比较低的产品就特别难体现出短期价值。 对于这个岗位的技能要求来说,没有要求你一定要从零开始实现所有的算法,现在有很多现 成的算法包进行调用。最基本的要求是,你要知道每个场景会用到哪个算法,比如分类场景, 常用的分类算法就有 LR/RF/Xgboost/ET 等等,此外,你还要知道每个算法的有效优化参数 是什么、模型效果不好的时候怎么优化。还需要有算法的实现能力,语言方面可以用 Scala/python/R/Java 等。我们常说:工具不重要,重要的是你玩工具,不是工具玩你。 另外针对有监督式学习算法,算法工程师最好有很好的业务 sense,这样在 feature 设计的 时候才能更有针对性,设计的 feature 才有可能有很好的先验性。 6、深度学习(NLP,CNN,语音识别) 这块我没具体商用过,只是动手实践过。个人感觉商业化是重点吧,特别是大家都在观望说 你的 chatbot 很有用啊,可是 siri 做了这么久,最后反响也一般。 现在客服机器人又很火,大家又在一通吐槽说,这个上下文理解的太差了,机器人的语义识 别做的怎么这么差。谁做谁知道,对于中文的语义识别,难度比国外的难多了,因为中文的 一种否定说法有太多种变体,你不知道我们会说哪种。 另外,常常有人吐槽说,你这个 CNN 这么复杂,我线上需要满足 100ms 内返回,搞的这么 复杂,实时调用怎么整,肯定来不及了,最后只能考虑 offline 预测了。常常说这话的人,是 不会自己写底层代码的,很多时候我觉得:不是你没有解决问题的办法,而是你没有去思考 怎么解决问题,心智决定了你的产出。 整体来说,这块对个人的综合素质要求是很高的。如果你只是想简单利用现成的 Model,提 取中间层的特征,然后再套用其他的机器学习模型进行预测的话,倒也能很好的解决一些现 实中的公司应用,比如 yelp 的图片分类。 不过,严格来说,这个不算是做深度学习的人,因为真正玩 DL 的人,是需要自己动手建模 型,调参数,改 symbol 的,所以他们的编程能力是很强的,这点上,我一直都高山仰止。 特别是一些创业公司,对于这个岗位的编程能力要求很高。如果你面试创业公司后没有下文 了那就表示:你很优秀,但是不一定适合我们公司,因为我们要找的编程能力很强的人。 这块我不专业,所以就点到为止,不说太多。个人认为,在这块上需要有比较强的算法改造 和优化能力,尽量的提高算法预测的速度,同时不断的提高算法的外延性提高精度,目前整 个行业也都是朝着好的方向在发展。如果有很多人看到这块行业开出来的高工资,记得和招 聘上的要求核对一下,自己哪块技能需要补充。这样你才能成为人中之凤。 对于未来,一片光明,对于未来,甚是期待,对于未来,一切可能。 大数据篇 132 做个总结: 以上说了这么多,唠叨了这么多,其实核心就是:如何用数据创造价值,如果你没有用数据 创造价值的能力,那么就只能等着被数据淹没,被数据拍死在职场上,早早到达职业的天花 板。 体现数据价值的层面上,越往数据应用层靠拢,对数据产生价值的要求就越高,从事这块领 域的人要常常自省是否有好的商业 Sense,毕竟在工业界,没人关心你是否比传统的 baseline 提高了一个百分点,他们关心的是你提高了一个百分点之后,对公司的价值是什么。 而越往底层那块,倒也没有强制要求和业绩绑定在一起,更多的是从流程上进行约定,对于 这块的价值体现,主要从技术层面上的创新为主,你如果解决了现存架构的问题,那么你就 可以成为一个大牛,所以多学学编程吧,别太约束自己,故步自封。 大数据篇 133 ElasticSearch 相关性打分机制 [作者简介]孙咸伟,携程技术中心市场营销研发部后端开发一枚。 携程旗下一新业务,主要给用户提供场馆预定。最近我们在做场馆搜索的功能时,接触到 elasticsearch(简称 es)搜索引擎。 我们展示给用户的运动场馆,在匹配到用户关键词的情况下,还会综合考虑多种因素,比如 价格,库存,评分,销量,经纬度等。 如果单纯按场馆距离、价格排序时,排序过于绝对,比如有时会想让库存数量多的场馆排名 靠前,有时会想让评分过低的排名靠后。有时在有多家价格相同的场馆同时显示的情况下, 想让距离用户近的场馆显示在前面,这时就可以通过 es 强大的评分功能来实现。 本文将分享 es 是如何对文档打分的,以及在搜索查询时遇到的一些常用场景,希望给接触 搜索的同学一些帮助。 一、Lucene 的计分函数(Lucene’s Practical Scoring Function) 对于多术语查询,Lucene 采用布尔模型(Boolean model)、词频/逆向文档频率(TF/IDF)、 以及向量空间模型(Vector Space Model),然后将他们合并到单个包中来收集匹配文档和 分数计算。 只要一个文档与查询匹配,Lucene 就会为查询计算分数,然后合并每个匹配术 语的分数。这里使用的分数计算公式叫做 实用计分函数(practical scoring function)。 score(q,d) = #1 queryNorm(q) #2 · coord(q,d) #3 · ∑ ( #4 tf(t in d) #5 · idf(t)² #6 · t.getBoost() #7 · norm(t,d) #8 ) (t in q) #9  1 score(q, d) 是文档 d 与 查询 q 的相关度分数  2 queryNorm(q) 是查询正则因子(query normalization factor)  3 coord(q, d) 是协调因子(coordination factor)  4 #9 查询 q 中每个术语 t 对于文档 d 的权重和  5 tf(t in d) 是术语 t 在文档 d 中的词频  6 idf(t) 是术语 t 的逆向文档频次  7 t.getBoost() 是查询中使用的 boost  8 norm(t,d) 是字段长度正则值,与索引时字段级的 boost 的和(如果存在) 大数据篇 134 词频(Term frequency) 术语在文档中出现的频度是多少?频度越高,权重越大。一个 5 次提到同一术语的字段比一 个只有 1 次提到的更相关。词频的计算方式如下: tf(t in d) = √frequency #1  1 术语 t 在文件 d 的词频(tf)是这个术语在文档中出现次数的平方根。 逆向文档频率(Inverse document frequency) 术语在集合所有文档里出现的频次。频次越高,权重越低。常用词如 and 或 the 对于相关 度贡献非常低,因为他们在多数文档中都会出现,一些不常见术语如 elastic 或 lucene 可 以帮助我们快速缩小范围找到感兴趣的文档。逆向文档频率的计算公式如下: idf(t) = 1 + log ( numDocs / (docFreq + 1)) #1  1 术语 t 的逆向文档频率(Inverse document frequency)是:索引中文档数量除以所有 包含该术语文档数量后的对数值。 字段长度正则值(Field-length norm) 字段的长度是多少?字段越短,字段的权重越高。如果术语出现在类似标题 title 这样的字 段,要比它出现在内容 body 这样的字段中的相关度更高。字段长度的正则值公式如下: norm(d) = 1 / √numTerms #1  1 字段长度正则值是字段中术语数平方根的倒数。 查询正则因子(Query Normalization Factor) 查询正则因子(queryNorm)试图将查询正则化,这样就能比较两个不同查询结果。尽管查 询正则值的目的是为了使查询结果之间能够相互比较,但是它并不十分有效,因为相关度分 数_score 的目的是为了将当前查询的结果进行排序,比较不同查询结果的相关度分数没有 太大意义。 查询协调(Query Coordination) 协调因子(coord)可以为那些查询术语包含度高的文档提供“奖励”,文档里出现的查询 术语越多,它越有机会成为一个好的匹配结果。 二、查询时权重提升(Query-Time Boosting) 大数据篇 135 在搜索时使用权重提升参数让一个查询语句比其他语句更重要。查询时的权重提升是我们可 以用来影响相关度的主要工具,任意一种类型的查询都能接受权重提升(boost)参数。将 权重提升值设置为 2,并不代表最终的分数会是原值的 2 倍;权重提升值会经过正则化和一 些其他内部优化过程。尽管如此,它确实想要表明一个提升值为 2 的句子的重要性是提升值 为 1 句子的 2 倍。 三、忽略 TF/IDF(Ignoring TF/IDF) 有些时候我们不关心 TF/IDF,我们只想知道一个词是否在某个字段中出现过,不关心它在 文档中出现是否频繁。 constant_score 查询 constant_score 查询中,它可以包含一个查询或一个过滤,为任意一个匹配的文档指定分数, 忽略 TF/IDF 信息。 function_score 查询(function_score Query) es 进行全文搜索时,搜索结果默认会以文档的相关度进行排序,如果想要改变默认的排序 规则,也可以通过 sort 指定一个或多个排序字段。但是使用 sort 排序过于绝对,它会直接 忽略掉文档本身的相关度。 在很多时候这样做的效果并不好,这时候就需要对多个字段进行综合评估,得出一个最终的 排序。这时就需要用到 function_score 查询(function_score query) ,它允许我们为每个 与主查询匹配的文档应用一个函数,以达到改变甚至完全替换原始分数的目的。 ElasticSearch 预定义了一些函数:  weight 为每个文档应用一个简单的而不被正则化的权重提升值:当 weight 为 2 时,最终结 果为 2 * _score  field_value_factor 使用这个值来修改 _score,如将流行度或评分作为考虑因素。  random_score 为每个用户都使用一个不同的随机分数来对结果排序,但对某一具体用户来说,看到的 顺序始终是一致的。  Decay functions — linear, exp, gauss 以某个字段的值为标准,距离某个值越近得分越高。  script_score 如果需求超出以上范围时,用自定义脚本完全控制分数计算的逻辑。 它还有一个属性 boost_mode 可以指定计算后的分数与原始的_score 如何合并,有以下 选项:  multiply 将分数与函数值相乘(默认)  sum 大数据篇 136 将分数与函数值相加  min 分数与函数值的较小值  max 分数与函数值的较大值  replace 函数值替代分数 field_value_factor field_value_factor 的目的是通过文档中某个字段的值计算出一个分数,它有以下属性:  field:指定字段名  factor:对字段值进行预处理,乘以指定的数值(默认为 1)  modifier 将字段值进行加工,有以下的几个选项: none:不处理 log:计算对数 log1p:先将字段值+1,再计算对数 log2p:先将字段值+2,再计算对数 ln:计算自然对数 ln1p:先将字段值+1,再计算自然对数 ln2p:先将字段值+2,再计算自然对数 square:计算平方 sqrt:计算平方根 reciprocal:计算倒数 假设有一个场馆索引,搜索时希望在相关度排序的基础上,评分(comment_score)更高的场 馆能排在靠前的位置,那么这条查询 DSL 可以是这样的: { "query": { "function_score": { "query": { "match": { "name": "游泳馆" } }, "field_value_factor": { "field": "comment_score", "modifier": "log1p", "factor": 0.1 }, "boost_mode": "sum" } }} 大数据篇 137 这条查询会将名称中带有游泳的场馆检索出来,然后对这些文档计算一个与评分 (comment_score)相关的分数,并与之前相关度的分数相加,对应的公式为: _score = _score + log(1 + 0.1 * comment_score) 随机计分(random_score) 这个函数的使用相当简单,只需要调用一下就可以返回一个 0 到 1 的分数。 它有一个非常有用的特性是可以通过 seed 属性设置一个随机种子,该函数保证在随机种子 相同时返回值也相同,这点使得它可以轻松地实现对于用户的个性化推荐。 衰减函数(Decay functions) 衰减函数(Decay Function)提供了一个更为复杂的公式,它描述了这样一种情况:对于一 个字段,它有一个理想的值,而字段实际的值越偏离这个理想值(无论是增大还是减小), 就越不符合期望。 有三种衰减函数——线性(linear)、指数(exp)和高斯(gauss)函数, 它们可以操作数值、时间以及 经纬度地理坐标点这样的字段。三个都能接受以下参数:  origin 代表中心点(central point)或字段可能的最佳值,落在原点(origin)上的文档分数为 满分 1.0。  scale 代表衰减率,即一个文档从原点(origin)下落时,分数改变的速度。  decay 从原点(origin)衰减到 scale 所得到的分数,默认值为 0.5。  offset 以原点(origin)为中心点,为其设置一个非零的偏移量(offset)覆盖一个范围,而不 只是原点(origin)这单个点。在此范围内(-offset <= origin <= +offset)的所有值的 分数都是 1.0。 这三个函数的唯一区别就是它们衰减曲线的形状,用图来说明会更为直观 衰减函数曲线 大数据篇 138 如果我们想找一家游泳馆:  它的理想位置是公司附近  如果离公司在 5km 以内,是我们可以接受的范围,在这个范围内我们不去考虑距离, 而是更偏向于其他信息  当距离超过 5km 时,我们对这家场馆的兴趣就越来越低,直到超出某个范围就再也不 会考虑了 将上面提到的用 DSL 表示就是: { "query": { "function_score": { "query": { "match": { "name": "游泳馆" } }, "gauss": { "location": { "origin": { "lat": 31.227817, "lon": 121.358775 }, "offset": "5km", "scale": "10km" } }, "boost_mode": "sum" } }} 我们希望租房的位置在(31.227817, 121.358775)坐标附近,5km 以内是满意的距离,15km 以 大数据篇 139 内是可以接受的距离。 script_score 虽然强大的 field_value_factor 和衰减函数已经可以解决大部分问题,但是也可以看出它们还 有一定的局限性: 这两种方式都只能针对一个字段计算分值 这两种方式应用的字段类型有限,field_value_factor 一般只用于数字类型,而衰减函数一般 只用于数字、位置和时间类型 这时候就需要 script_score 了,它支持我们自己编写一个脚本运行,在该脚本中我们可以拿 到当前文档的所有字段信息,并且只需要将计算的分数作为返回值传回 Elasticsearch 即可。 注:使用脚本需要首先在配置文件中打开相关功能: script.groovy.sandbox.enabled: true script.inline: on script.indexed: on script.search: on script.engine.groovy.inline.aggs: on 现在正值炎热的夏天,游泳成为很多人喜爱的运动项目,在满足用户搜索条件的情况下,我 们想把游泳分类的场馆排名提前。此时可以编写 Groovy 脚本(Elasticsearch 的默认脚本语 言)来提高游泳相关场馆的分数。 return doc['category'].value == '游泳' ? 1.5 : 1.0 接下来只要将这个脚本配置到查询语句: { "query": { "function_score": { "query": { "match": { "name": "运动" } }, "script_score": { "script": "return doc['category'].value == '游泳' ? 1.5 : 1.0" } } }} 当然还可以通过 params 属性向脚本传值,让推荐更灵活。 { 大数据篇 140 "query": { "function_score": { "query": { "match": { "name": "运动" } }, "script_score": { "params": { "recommend_category": "游泳" }, "script": "return doc['category'].value == recommend_category ? 1.5 : 1.0" } } }} scirpt_score 函数提供了巨大的灵活性,我们可以通过脚本访问文档里的所有字段、当前评 分甚至词频、逆向文档频率和字段长度正则值这样的信息。 同时使用多个函数 上面的例子都只是调用某一个函数并与查询得到的_score 进行合并处理,而在实际应用中肯 定会出现在多个点上计算分值并合并,虽然脚本也许可以解决这个问题,但是应该没人愿意 维护一个复杂的脚本。 这时候通过多个函数将每个分值都计算出再合并才是更好的选择。 在 function_score 中可 以使用 functions 属性指定多个函数。它是一个数组,所以原有函数不需要发生改动。同时 还可以通过score_mode 指定各个函数分值之间的合并处理,值跟最开始提到的boost_mode 相同。 下面举个例子介绍多个函数混用的场景。我们会向用户推荐一些不错的场馆,特征是:范围 要在当前位置的 5km 以内,有停车位很重要,场馆的评分(1 分到 5 分)越高越好,并且对 不同用户最好展示不同的结果以增加随机性。 那么它的查询语句应该是这样的: { "query": { "function_score": { "filter": { "geo_distance": { "distance": "5km", "location": { "lat": $lat, "lon": $lng } } }, "functions": [ { 大数据篇 141 "filter": { "term": { "features": "停车位" } }, "weight": 2 }, { "field_value_factor": { "field": "comment_score", "factor": 1.5 } }, { "random_score": { "seed": "$id" } } ], "score_mode": "sum", "boost_mode": "multiply" } }} 注:其中所有以$开头的都是变量。 这样一个场馆的最高得分应该是 2 分(有停车位)+ 7.5 分(评分 5 分 * 1.5)+ 1 分(随机评分)。 总结 本文主要介绍了 Lucene 是如何基于 TF/IDF 生成评分的,以及 function_score 的使用。实 践中,简单的查询组合就能提供很好的搜索结果,但是为了获得具有成效的搜索结果,就必 须反复推敲修改前面介绍的这些调试方法。 通常,经过对策略字段应用权重提升,或通过对查询语句结构的调整来强调某个句子的重要 性这些方法,就足以获得良好的结果。有时,如果 Lucene 基于词的 TF/IDF 模型不再满足 评分需求(例如希望基于时间或距离来评分),则需要使用自定义脚本,灵活应用各种需求。 大数据篇 142 携程机票大数据架构最佳实践 [作者简介]许鹏,携程机票大数据基础平台 Leader,负责平台的构建和运维。深度掌握各种 大数据开源产品,如 Spark、Presto 及 Elasticsearch。著有《Apache Spark 源码剖析》一书。 本文来自许鹏在〖DAMS 2017 中国数据资产管理峰会〗上的分享,首发 DBAplus 社群(ID: dbaplus)。 现如今大数据一块有很多的开源项目,因此首先搭建平台的难点其实在于如何选择一个合适 的技术来做整个平台的架构,第二,因为有业务数据,用了平台之后的话,如何用平台把数 据分析出来让用户有很好的交互性的体验。第三个层面就是理工科喜欢建模,而在这整个过 程当中,我们会形成一种非数据建模,而主要是我们如何分不同层面的人员搭配,进而做成 这样一个大数据团队。 一、数据平台技术选型 1、整体框架 这个框架应该是一种大路货,或者更认为是一种比较常见的架构。前面也就是从数据源到消 息队列到数据的清理、数据呈现等这些大家容易想到的东西,而在这样一个大帽子下面,所 不一样的东西是具体选用什么样的组件来填这个空,在不同的场景下,每个人的选择是不大 相同的。 像消息队列这一层,我们选用了 Kafka,这是目前大家普遍用到的,因为它有高吞吐量,采 用 Push 和 Pull 结合的方式,消费端主动拉取数据。ETL 这块,目前大家都希望采用一种可 以自定义的方式,一般来说比较流行的是用 LinkedIn 提供的 Camus 来做从 Kafka 到 HDFS 的数据同步。这应该是一种较为流行的架构。 那么放到 HDFS 上面的数据,基本上是为了批处理做准备的,那么在批处理分析的时候,我 们选择一个什么样的分析引擎,可能就是一个值得争议的焦点,也就是说,也许在这个分析 大数据篇 143 引擎的下面,有 Hive,有 Spark,有 Presto,有 Impala,还有其它的东西。 在这些引擎当 中的选择或者实践,需要结合具体使用场景。 下面讲讲为什么会选择 Presto 而不是其它。假设在座的各位有 Presto 使用经验的话,会发 觉 Presto 它是一个 CLI 的用户界面,并没有好的一种 Web UI,对一般用户来说,CLI 的使用 会有难度,不管这是感觉上的还是实际上的,所以需要有个好的 Web UI 来增加易用性。 当前在 GitHub 上面能找到的 Presto webui 的就是 Airbnb 提供的 AirPal,但根据我们的使用 经验,不怎么友好,特别在 UTC 的时间设置上,同时它的社区维护已停滞在两年前,这一 块我们做了适配,然后用 Presto 的 StatementClient 做了 Web UI。前端采用的是 jquery 的 easyui, 像刚才讲的批处理这一条线,就是用在了批处理这一块上。下面这一条线就是说有 些数据可能是希望立马存储,立即被搜索到,或者做简要的分析。 作为搜索引擎,社区这一块,大家耳熟能详的应该是 Elasticsearch,Elasticsearch 的社区非 常活跃,而且它的推广速度,应用型上面易都很好。但是 Elasticsearch 的难点在于如何对它 进行好的维护,后面我会讲到它可能存在的维护痛点。 那么,Elasticsearch 有非常强大的搜索能力,响应时间也是非常快的,但是它的用户接口, 有自己的一套基于 Lucene 的搜索语法,当然 Lucene 的这一套语法本身是非常极客的,很 简洁,但是一般的人不愿意去学这个东西,因为对于分析师来讲去学,就意味着以前的武功, 几十年功夫白费了。 于是我们就采用了一个插件 Elastisearch-SQL,这样就可以采用 SQL 语句对 Elasticsearch 进 行点查询或者范围查询。而且在 Elasticsearch 的演进路径当中,也会支持 SQL,按照之前看 到的 ES roadmap, 应该在 17 年最迟不超过 18 年发布 6.×,重要的特性之一是对 SQL 的支 持,大家可以看到如果不支持 SQL,就等于是自废武功,或者拒客户于千里之外。 WebUI 是人机交互的部分,我们会进行 Ad-hoc 查询,但在整个部门当中有不少程序希望调 用查询,也就是应用的接口,采用 SOA 的架构,我们自己开发实现了 BigQuery API,可以 通过这种调 Restful 接口方式,进行取数或者分析。那么我们会自动判别到底是到 ES 这一 侧还是到 Presto 进行取数。 在很多公司的使用当中,数据分析这一块是需要报表的,就是要有很好的 Dashboard。 2、ETLPipeLine -- Gobblin 大数据篇 144 这个是 ETL 相对比较细节的一些东西。快速过一下这个图。在 ETL 的时间当中,比如说为什 么不直接用像 Spark 或者流的方式,最常见的问题就是小文件的问题,到时候需要清理合并 小文件,这很麻烦。如果采用 Zeus 去调度,然后设定一定数目的 Partition,就有一个 Map Task 对应,尽可能的写满一个 Block,以 64M 或者 128M 为主。在存储的时候我们除了考虑 它的大小之外,存储格式的选择也应该是必须考量的范围。 从我们当前的选择来看,建议使用 ORC 这样的文件格式,采用这个文件格式是由于它已经 内嵌了一定级别的索引,尽管索引不是非常细粒度,但是在某些层面是能够急速地提高检索, 跳过不符合条件的数据块,避免不必要的数据传输。目前相对比较有希望的,或者大力推广 的一个格式就是华为公司在推的 CarbonData,它含有的索引粒度,索引信息比 ORC 更加细 致。他们目前也出了 1.×的版本,是相对来讲较为成熟一个版本。 3、分析引擎 -Presto 大数据篇 145 这里讲的是 Presto 的内部机理。为什么不用 Hive 和 Spark?Hive 相当于是俄国的武器,特 点就是傻大黑粗,绝对的稳定,稳定到什么程度?稳定到就是它是最慢的一个,有一个笑话 就是我的成绩一直很稳定,因为老考倒数第一,没人可以比过,所以一直很稳定,而正数第 一不见得很稳定,Hive 就是这个特点,绝对可以出来结果,但是会让你觉得人生没有指望。 Spark 的特点就是它名头绝对的够响,但是会发觉 Spark 具体的使用过程当中有些问题?资 源共享是一个问题,如果说你光用 Spark,肯定 Concurrent Query 出现问题的,要前置一个 东西,比如 Livy 或者什么东西来解决掉你的资源共享问题。而且 Spark 的雄心很大,几乎想 把所有东西都吃下去,所有东西都吃,就很难,因为你要涉及很多的领域。 Presto 只专注于数据的分析,只关注 SQL 查询层面,只做一件事,这个充分体现了 Unix 的 哲学,遵循只干一件活,不同的活通过 Pipeline 的方式串起来。而且 Presto 是基于流水线 的,只要有一个块当中结果出来了,然后比如说我们最典型的就是后面加一个后置的条件, 然后 limit 10 或者 Limit 1,你会发觉很快出来结果,用 Spark 会发现它 Where 条件的搜索会 经历多个 Stage,必须到前面的 Stage 都完成了才可以跑下一个 Stage, 那个 Limit 1 的结 果要到后面才过滤。 大数据篇 146 从 Presto 后面给出的这些数据可以看到,这种层面上的一个提升。基于 ORC 的文件存储, 它的提升应该是 5 倍或者 10 倍,10 倍到 20 倍的提升。它的架构简单来说是有一个 Client, 然后这个 Client 提交 SQL 语句过来,前面有一个 Planner 和 Scheduler,会把相应的 SQL 的 东西分层,分成不同的 Stage,每一个 Stage 有多个 Task,这些真正的 Task 是运行在不同 的 Workers 上面,利用这些 Workers 去数据源读取数据。 也就是说 Presto 是专注于在数据分析这侧,具体数据的存储在外面,这个当中肯定要去解 决哪些东西是值得去拉取的,有哪些东西可以直接推到数据源侧去搞定,不需要傻乎乎地把 很多东西拉上来。 分析引擎比较——Presto 与 MapReduce 大数据篇 147 大家可以看到我刚才提到一个基于 Stage 的方式,一个基于 Pipeline 的方式,Pipeline 的方 式就是整个过程中,处理没有停顿,整个是交叉的,它不会等上一个 Stage 完成后再进行下 一个 Stage,Spark 的特点就是等到一个 Stage 结束了,数据吐到 Disk 中,下一个 Stage 再 去拉数据,然后再进行下一个。Pipeline 就是说我有一个 Task 处理完,直接将数据吐到下一 个 Task,直到 Aggregator 节点。 那么在这个过程当中,你也会看到 Presto 的一个最大特点就在于所有的计算就在内存当中, 你会想到人的大脑,机器的内存都是有限的,会崩掉了,崩掉就崩掉了,早死早超生,大不 了再跑一趟,这就是 Presto 的一个基本原则。 MapReduce 会重启,如果成功了还好,重启很多次崩掉是不是三观尽毁?通过这种特点也 表明 Presto 适用的场景,适用于交互式查询,如果是批量的,你晚上要做那种定期报表的 话,把整个交给 Presto 是不负责任的表现,因为有大量的时间,应该给 Hive 比较好。 4、近实时搜索 –Elasticsearch 下面讲讲 ES 层面的东西,也就是近实时的搜索引擎,它所有的东西都是基于 Lucene 上面 进行一个包裹,对 JSON 支持的非常好。同时 Elasticsearch 支持横向、水平扩展,高可用, 易于管理,社区很活跃,背后有专门的商业公司。它的竞品就是 Solr,Solr 的 Cloud,SolrCloud 安装较为复杂,引入了独立的第三方东西,对 ZooKeeper 集群有极大的依赖,这样使得 Solr Cloud 的管理变得复杂。 SolrCloud 的发展也很活跃,现在是到了 6.×,后续就是到 7.×,而且 SolrCloud 的 6.×当中 引入了对 SQL 的支持,ES 和 SolrCloud 是同门师兄弟,通过同门师兄弟的相互竞争可以看 到发展的趋势——SQL 一定是会支持的。 大数据篇 148 如果大家做搜索这一块东西的话,上面这张图其实是很常见的,它肯定会在某一个节点上面 有相应的一个主分区,有一个 Primary partition,而在另外一个节点上面它有一个 Replicas, 而且 Replica 可能不只一个,如果这些没有,这张图就没有太多好讲的。问题是该分几个 Replica,在每台机器上分几个不同的 partition,如果在从事维护工作的话,上述问题是值得 去分析和考究的。 ES 调优和运维 下面讲 ES 的调优和运维,从两个层面出发。 第一个层面就是 OS, 讲到 Linux, 调优过程中自然会考虑到它的文件句柄数,然后它的 Memory,它的 I/O 的调度,I/O 的调度线如果在座各位对内核比较感兴趣的话,你会发现基 本使用 CFQ,因为在生产环节上大多会采用 Redhat 或者 CentOS 来部署,不会部署到像自 己玩的 Archlinux 或者 Gentoo 上面,不可能这样做的。 还有就是它的 Virtual memory DirtyRatio,这个东西是会极大地影响响应时间,或者说有时 你会发觉 I/O 操作,而且 CPU 一直比较高,因为有文件缓存,缓存足够多的话就一直往磁 大数据篇 149 盘去写,所以我们的办法就是把原来设置比较高的 vm.dirty_ratio,由默认 20%调小到 10%。 意思就是说缓存内容一旦超过系统内存的 10%其它活不要干了,专心致志吐这个缓存内容。 Vm.dirty_background_ratio 是说如果达到这个阀值,就开始将文件缓存内容写入到磁盘。OS 层面的调优和数据库的系统调优有相似性。 另一个层面的调优是 ES 本身,首先就是说我在一个 Cluster 上,Shard 的数目要均匀分布。 我这里放了一张截图,这个截图大家可以看到所有的节点上面,Shard 数目上来讲是非常均 匀的。有相应的参数调整可以达到这样的效果。第二个就是会有一个 Replica 的过程,比如 新加一台机器或者说我是减少一台机器,要做相应的维护,机器的集群会做动态的扩容和缩 减。那么这时如果都来做 Shard 的转移,整个集群的写入和查询会受很大影响,所以做一定 的均衡,两者之间要有一定的 Balance。这些讲的都是集群级别,下面讲索引级别的优化。 索引级别的优化就是我要对 Shard 的数目,到底是这个 Index 是分十个 Shard 存还是 5 个来 存,refresh 的频率,Refresh 就是说这个数据写入多久之后可以被搜索到。Refresh 时间拉得 越长,数据吞吐量越大,但是可以被搜索到的时间越滞后。还有 Merge 的过程,因为分片, 为了减少对文件句柄使用, 所以需要进行 Merge。有人讲就是因为 ES 支持 Schemaless 了, 所以不需要 fixed 的 Schema。但在实际的使用过程中发觉,如果不做一定限制的话,每个 人都认为是自由的,就会出现一个 Field 的急速膨胀,在某个索引下面成千上万的字段, 这 样一来索引的写入速度就下来了。 下图是我们自己写的 Dashboard,说到 ES,可能在座的也有不少在用,如果说你们升级到 5.×后发现一点,1.×比较好的插件 Marvel,5.×里面就没有,提供的就是 X-pack,X-pack 是 要收钱的,那么它同时提供了一个所谓的 basic 版本,Free 的东西大家都知道,便宜无好货, 就是说它的功能是对比了 1.×的版本,很多信息都是没有的。 大数据篇 150 我们的话就是自力更生,因为你所有的内容都是可以通过 Rest API 读取到,只不过是需要在 前端可视化一下。那么这张图就是我们做的工作,可以很方便地看到当前节点的写入量、查 询量,当前节点的索引,Shard 数目还有当前集群的状态,如果一旦状态变为 red,可以邮 件通知,在页面上还可以进一步点下去了解每一个节点和索引的详细信息。 稍微总结一下,一般来说在调优上考量的不外乎四个维度:一个 CPU 的维度,一个 Memory 的角度,还有就是 Disk 的 I/O 角度,另外一个是网络。 比如从原来的百 M 网卡升级到千 M 网卡,从千 M 到万 M,查询的响应速度会有很大提升。 大数据篇 151 这是前面提到我们统一的一个 SQL 查询的接口,大家可以看到这挺简陋的,很傻很天真的 样子,我就是上面输入一个 SQL,下面很快就出来一个结果。但就是因为采用了这种方式, 因为后面是它采用了 Presto 这个引擎,在部门内部,我们有不少同事都在使用这个进行数 据查询,目前的日常使用量应该是在近 8K 的样子,因为最近还升级了一下网卡,升级到万 M 网卡,使得速度更加快。多余的时间喝喝咖啡抽抽烟生活多美好,比等在那里焦虑有意思 多了。 5、数据可视化——Zeppelin 在做数据可视化这一块时,可以借鉴竞争对手或者竞品,看看别人在做什么,如果说大家去 看 Hue, Hue 的话,其实就是上面输入查询语句之后,后续就把结果很好地显示出来。我 们目前所考虑的就是说如何做到 Data visualize 的,目前尝试用 Zeppelin,这个可以通过 JDBC 接口对接 Presto,把数据查询出来,通过简单的拖拽,直接把报表以图形化的方式展现出来。 补充一下,Zeppelin 这个如果要对接 Spark,如果只是一个 Spark 集群,直接对接这个 Spark 集群,资源利用率是非常非常低的,但是你在前置一个 Livy Server 的话,通过 Livy 来进行 资源调度,资源共享会比较好。目前有两个这一方面的竞品,一个 Livy,另外一个就是 Oyala 它提供的 SparkJob ServerS,干的活其实都是一样。Zeppelin 是对 Livy Server 做了整合。 大数据篇 152 6、数据微服务 –Rest 查询接口 微服务这一块,我们提供了一个 BigQuery API,这样的好处是有一个统一的查询入口,有统 一的权限管理。因为查询时不是所有人都应该看到所有的数据,这很容易出问题,可能有比 较实实在在的数据,它不像一般的日志数据,特别像机票或者我们这边的酒店,它的数据有 不少的一些敏感信息,这需要做相应的权限管理。 这个入口统一之后,做权限管理就比较方便了,出问题的话只要查相应的日志就 OK 了。而 且使用的是统一的查询语言,都用的是大家比较熟知的这种 SQL 语句,不是说用了一个新 的东西就要学习一套新的知识,这样子的话原有知识不容易得到传承,这是大家都应尽量去 避免的。 7、任务调度器 –Job Scheduler Zeus-https://github.com/ctripcorp/dataworks-zeus 其实在做一套大数据的平台时,少不了任务调度这一块。任务调度这一块我们使用的是 Zeus 系统,携程在这一块开源出来,由我们公司 Ops 的团队专门来负责开发和维护个平台。但 是你想,通过这个平台递交的任务包括,ETL 和定时任务,可以实现将数据从 Kafka 放入到 HDFS 或者是把 SQL Server 和 MySQLDB 里面的数据同步到 HDFS。调度这一块目前市面上 的竞品还有 AirFlow 和其它。 大数据篇 153 二、数据团队能力建设 这部分讲的是我们团队的建设。目前我把它分成五个不同的角度,第一个是引擎的开发,这 一块是相对较难的,它对后台的技术要求比较高。 第二是交互界面设计,整个东西做出来,如果只是做了引擎,或者对引擎做了,但是没有实 际的人用,老板肯定也会叫滚蛋的,肯定要一环套一环,形成有效的传动,不是单点,只讲 发动机没有任何意义的,要讲整车。所以有引擎,引擎的要求也比较高,会有一个交互界面 的设计,就是我如何用这些引擎的东西。 把这些东西都弄上后,可以转起来了,整个可以转起来之后,我们还有个运维,其实大家可 以逐步发现一个趋势,就是无论大数据也好,云平台也好,对运维的要求都是比较高的,一 个好的运维不仅要掌握一个基础的 OS 层面的东西,对后台也得有一个较好的概念或者好的 研究。无论是从后台服务开发转到运维还是从运维转后台服务器开发,两者都需要去交叉学 习。 那么,一个平台规划相对来说就是一个架构师或相对更高层一点人员的工作范畴,视野可以 更高一点,这样的角色肩负了架构和产品经理这两个概念的东西,因为像这种东西最主要是 内部使用,比较难以独立出来。 语言这一块就是见仁见智,我只是把我们现在采用到的,使用到的东西列了一下,有上述这 么多。 大体我们的实践的就是这些。我们所有的部分应该就在这一张图里,这张图的内容看起来比 较平淡,但是如果需要把这张图弄好,确实花了不少时间。 大数据篇 154 大数据篇 155 大规模知识图谱的构建、推理及应用 [作者简介]李健,携程度假研发部研发总监,2013 年底加入携程,在数据挖掘分析、人工智 能方面有一定的实践与积累。 随着大数据的应用越来越广泛,人工智能也终于在几番沉浮后再次焕发出了活力。除了理论 基础层面的发展以外,本轮发展最为瞩目的是大数据基础设施、存储和计算能力增长所带来 的前所未有的数据红利。 人工智能的进展突出体现在以知识图谱为代表的知识工程以及以深度学习为代表的机器学 习等相关领域。 未来伴随着深度学习对于大数据的红利消耗殆尽,如果基础理论方面没有新的突破,深度学 习模型效果的天花板将日益迫近。而另一方面,大量知识图谱不断涌现,这些蕴含人类大量 先验知识的宝库却尚未被深度学习有效利用。 融合知识图谱与深度学习,已然成为进一步提升深度学习效果的重要思路之一。以知识图谱 为代表的符号主义,和以深度学习为代表的联结主义,日益脱离原先各自独立发展的轨道, 走上协同并进的新道路。 一、大规模知识图谱的构建 知识图谱自上世纪 60 年代从语义网络发展起来以后,分别经历了 1980 年代的专家系统、 1990 年代的贝叶斯网络、2000 年代的 OWL 和语义 WEB,以及 2010 年以后的谷歌的知识 图谱。谷歌目前的知识图谱已经包含了数亿个条目,并广泛应用于搜索、推荐等领域。 大数据篇 156 知识图谱的存储和查询语言也经历了历史的洗涤,从 RDF 到 OWL 以及 SPARQL 查询,都逐 渐因为使用上的不便及高昂的成本,而被工业界主流所遗弃。图数据库逐步成为目前主要的 知识图谱存储方式。 目前应用比较广泛的图数据库包括 Neo4J、graphsql、sparkgraphx(包含图计算引擎)、基 于 hbase 的 Titan、BlazeGraph 等,各家的存储语言和查询语言也不尽相同。实际应用场景 下,OrientDB 和 postgresql 也有很多的应用,主要原因是其相对低廉的实现成本和性能优 势。 由于大规模知识图谱的构建往往会有众多的实体和关系需要从原始数据(可以是结构化也可 以是非结构化)中被抽取出来,并以图的方式进行结构化存储,而我们依赖的原始数据往往 存在于多源异构的环境中,所以进行海量知识抽取和融合,就成了首要的无法回避的严峻问 题。 对于结构化的数据转换为图结构是比较容易和相对轻松的工程,所以建议这一步应该首先被 大数据篇 157 完成。 对于复杂的非结构化数据,现阶段进行知识图谱构建的主要方法有传统 NLP 和基于深度学 习模型两类方法,而目前越来越多倾向于使用深度学习来抽取 AVP(属性-值对)。 有很多深度学习模型可以用来完成端到端的包括命名实体识别 NER、关系抽取和关系补全 等任务,从而构建和丰富知识图谱。 命名实体识别(Named Entity Recognition, NER)是从一段非结构化文本中找出相关实体 (triplet 中的主词和宾词),并标注出其位置以及类型,它是 NLP 领域中一些复杂任务(如 关系抽取、信息检索等)的基础。 NER 一直是 NLP 领域的热点,从早期基于字典和规则的方法,到传统机器学习的方法,再 到近年来基于深度学习的方法,NER 方法的大致演化如下所示。 在机器学习中,NER 被定义为序列标注问题。不同于分类问题,序列标注问题中的预测标签 不仅与输入特征有关,还与之前的预测标签有关,也就是预测标签之间存在相互依赖和影响。 条件随机场(Conditional Random Field,CRF)是序列标注的主流模型。它的目标函数不仅 考虑输入的状态特征函数,还包含了标签转移特征函数。在训练的时候可以使用 SGD 学习 大数据篇 158 参数。在预测时,可以使用 Vertibi 算法求解使目标函数最大化的最优序列。 目前常见的基于深度学习的序列标注模型有 BiLSTM-CNN-CRF。它主要由 Embedding 层(词 向量、字向量等)、BiLSTM、tanh 隐藏层以及 CRF 层组成(对于中文可以不需要 CNN)。我 们的实验表明 BiLSTM-CRF 可以获得较好的效果。在特征方面,由于秉承了深度学习的优点, 所以无需特征工作的铺垫,使用词向量及字向量就可以得到不错的效果。 近几个月来,我们也在尝试使用 Attention 机制,以及仅需少量标注样本的半监督来进行相 应的工作。 在 BiLSTM-CRF 的基础上,使用 Attention 机制将原来的字向量和词向量的拼接改进为按权 重求和,使用两个隐藏层来学习 Attention 的权值,这样使得模型 可以动态地利用词向量和字向量的信息。同时加入 NE 种类的特征,并在字向量上使用 Attention 来学习关注更有效的字符。实验效果优于 BiLSTM-CRF 的方法。 这里之所以用了大量篇幅来说 NER 的深度学习模型,是因为关系抽取模型也是采用同样的 模型实现的,其本质也是一个序列标注问题。所以这里不再赘述。 知识图谱构建中的另外一个难点就是知识融合,即多源数据融合。融合包括了实体对齐、属 性对齐、冲突消解、规范化等。对于 Open-domain 这几乎是一个举步维艰的过程,但是对 于我们特定旅游领域,可以通过别名举证、领域知识等方法进行对齐和消解,从技术角度来 看,这里会涉及较多的逻辑,所以偏传统机器学习方法,甚至利用业务逻辑即可覆盖大部分 场景。 知识图谱 schema 是知识的分类体系的表现,还可以用于逻辑推理,也是用于冲突检测的方 法之一,从而提高知识图谱的质量。 总而言之,构建知识图谱没有统一的方法,因为其构建需要一整套知识工程的方法,需要用 大数据篇 159 到 NLP、ML、DL 技术,用到图数据库技术,用到知识表示推理技术等。知识图谱的构建就 是一个系统工程,而且知识的更新也是不可避免的,所以一定要重视快速迭代和快速产出检 验。 二、知识图谱的推理 在知识图谱构建过程中,还存在很多关系补全问题。虽然一个普通的知识图谱可能存在数百 万的实体和数亿的关系事实,但相距补全还差很远。知识图谱的补全是通过现有知识图谱来 预测实体之间的关系,是对关系抽取的重要补充。传统方法 TransE 和 TransH 通过把关系作 为从实体 A 到实体 B 的翻译来建立实体和关系嵌入,但是这些模型仅仅简单地假设实体和 关系处于相同的语义空间。而事实上,一个实体是由多种属性组成的综合体,不同关系关注 实体的不同属性,所以仅仅在一个空间内对他们进行建模是不够的。 因此我们尝试用 TransR 来分别将实体和关系投影到不同的空间中,在实体空间和关系空间 构建实体和关系嵌入。对于每个元组(h,r,t),首先将实体空间中的实体通过 Mr 向关系 r 投 影得到 hr 和 tr,然后是 hr+r ≈tr。特定的关系投影能够使得两个实体在这个关系下真实地 靠近彼此,使得不具有此关系的实体彼此远离。 大数据篇 160 知识图谱推理中还经常将知识图谱表示为张量 tensor 形式,通过张量分解(tensor factorization)来实现对未知事实的判定。常用于链接预测(判断两个实体之间是否存在某 种特定关系)、实体分类(判断实体所属语义类别)、实体解析(识别并合并指代同一实体的 不同名称)。 常见的模型有 RESCAL 模型和 TRESCAL 模型。 RESCAL 模型的核心思想,是将整个知识图谱编码为一个三维张量,由这个张量分解出一个 核心张量和一个因子矩阵,核心张量中每个二维矩阵切片代表一种关系,因子矩阵中每一行 代表一个实体。由核心张量和因子矩阵还原的结果被看作对应三元组成立的概率,如果概率 大于某个阈值,则对应三元组正确;否则不正确。 而 TRESCAL 则是解决在输入张量高度稀疏时所带来的过拟合问题。 路径排序算法也常用来判断两个实体之间可能存在的关系,比如 PRA 算法。本文不展开描 述。 三、大规模知识图谱的应用 知识图谱的应用场景非常广泛,比如搜索、问答、推荐系统、反欺诈、不一致性验证、异常 分析、客户管理等。由于以上场景在应用中出现越来越多的深度学习模型,因此本文主要讨 论知识图谱在深度学习模型中的应用。 目前将知识图谱用于深度学习主要有两种方式,一种是将知识图谱的语义信息输入到深度学 习模型中,将离散化的知识表示为连续化的向量,从而使得知识图谱的先验知识能够称为深 度学习的输入;另外一种是利用知识作为优化目标的约束,指导深度学习模型的学习过程, 通常是将知识图谱中的知识表示为优化目标的后验正则项。 知识图谱的表示学习用于学习实体和关系的向量化表示,其关键是合理定义知识图谱中关于 事实(三元组 h,r,t)的损失函数 fr(h,t),其总和是三元组的两个实体 h 和 t 的向量化表示。 通常情况下,当事实 h,r,t 成立时,期望最小化 fr(h,t)。 大数据篇 161 常见的有基于距离和翻译的模型。 基于距离的模型,比如 SE 模型,其基本思想是当两个实体属于同一个三元组时,它们的向 量表示在投影后的空间中也应该彼此靠近。所以损失函数定义为向量投影后的距离 其中矩阵 Wr1 和 Wr2 用于三元组中头实体 h 和尾实体 t 的投影操作。 基于翻译的模型可以参考前述的 TransE, TransH 和 TransR 模型。其通过向量空间的向量翻 译来描述实体与关系之间的相关性。 当前的知识图谱表示学习方法都还存在各种问题,这个领域的发展也非常迅速,值得期待。 知识图谱的表示转换后,根据不同领域的应用,就可以和各种深度学习模型相结合,比如在 自动问答领域,可以和 encoder-decoder 相结合,将问题和三元组进行匹配,即计算其向量 相似度,从而为某个特定问题找到来自知识图谱的最佳三元组匹配。也有案例在推荐系统中, 通过网络嵌入(network embedding)获取结构化知识的向量化表示,然后分别用 SDAE (Stacked Denoising Auto-Encoder)和层叠卷积自编码器(StackedConvolutional Auto- Encoder)来抽取文本知识特征和图片知识特征,并将三类特征融合进协同集成学习框架, 利用三类知识特征的整合来实现个性化推荐。 随着深度学习的广泛应用,如何有效利用大量先验知识,来大大降低模型对大规模标注语料 的依赖,也逐渐成为主要的研究方向之一。在深度学习模型中融合常识知识和领域知识,将 是又一大机遇和挑战。 大数据篇 162 如何基于 Spark Streaming 构建实时计算平台 [作者简介]潘国庆,携程大数据平台实时平台主要负责人,毕业于昆士兰大学获得软件工程 专业硕士学位,2016 年加入携程,主要从事大数据领域的相关研究,在实时领域中积累了 诸多经验。 一、前言 随着互联网技术的迅速发展,用户对于数据处理的时效性、准确性与稳定性要求越来越高, 如何构建一个稳定易用并提供齐备的监控与预警功能的实时计算平台也成了很多公司一个 很大的挑战。 自 2015 年携程实时计算平台搭建以来,经过两年多不断的技术演进,目前实时集群规模已 达上百台,平台涵盖各个 SBU 与公共部门数百个实时应用,全年 JStorm 集群稳定性达到 100%。目前实时平台主要基于 JStorm 与 Spark Streaming 构建而成,相信关注携程实时平台 的朋友在去年已经看到一篇关于携程实时平台的分享:携程实时大数据平台实践分享。 本次分享将着重于介绍携程如何基于 Spark Streaming 构建实时计算平台,文章将从以下几 个方面分别阐述平台的构建与应用: Spark Streaming vs JStorm Spark Streaming 设计与封装 Spark Streaming 在携程的实践 曾经踩过的坑 未来展望 二、Spark Streaming vsJStorm 携程实时平台在接入 Spark Streaming 之前,JStorm 已稳定运行有一年半,基本能够满足大 部分的应用场景。接入 Spark Streaming 主要有以下几点考虑:首先携程使用的 JStorm 版本 为 2.1.1 版本,此版本的 JStorm 封装与抽象程度较低,并没有提供 High Level 抽象方法以及 对窗口、状态和 Sql 等方面的功能支持,这大大的提高了用户使用 JStorm 实现实时应用的 门槛以及开发复杂实时应用场景的难度。在这几个方面,SparkStreaming 表现就相对好的多, 不但提供了高度集成的抽象方法(各种算子),并且用户还可以与 SparkSQL 相结合直接使 用 SQL 处理数据。 其次,用户在处理数据的过程中往往需要维护两套数据处理逻辑,实时计算使用 JStorm,离 线计算使用 Hive 或 Spark。为了降低开发和维护成本,实现流式与离线计算引擎的统一, Spark 为此提供了良好的支撑。 最后,在引入 Spark Streaming 之前,我们重点分析了 Spark 与 Flink 两套技术的引入成本。 Flink 当时的版本为 1.2 版本,Spark 的版本为 2.0.1。相比较于 Spark,Flink 在 SQL 与 MLlib 大数据篇 163 上的支持相对弱于 Spark,并且公司许多部门都是基于 Spark SQL 与 MLlib 开发离线任务与 算法模型,使得大大降低了用户使用 Spark 的学习成本。 下图简单的给出了当前我们使用 Spark Streaming 与 JStorm 的对比: 三、Spark Streaming 设计与封装 在接入 Spark Streaming 的初期,首先需要考虑的是如何基于现有的实时平台无缝的嵌入 SparkStreaming。原先的实时平台已经包含了许多功能:元数据管理、监控与告警等功能, 所以第一步我们先针对 SparkStreaming 进行了封装并提供了丰富的功能。整套体系总共包 含了 Muise Spark Core、Muise Portal 以及外部系统。 3.1 Muise Spark Core MuiseSpark Core 是我们基于 Spark Streaming 实现的二次封装,用于支持携程多种消息队 列,其中 HermesKafka 与源生的 Kafka 基于 Direct Approach 的方式消费数据,Hermes Mysql 与 Qmq 基于 Receiver 的方式消费数据。接下来将要讲的诸多特性主要是针对 Kafka 类型的 数据源。 大数据篇 164 Muisespark core 主要包含了以下特性: Kafka Offset 自动管理 支持 Exactly Once 与 At Least Once 语义 提供 Metric 注册系统,用户可注册自定义 metric 基于系统与用户自定义 metric 进行预警 Long running on Yarn,提供容错机制 3.1.1 Kafka Offset 自动管理 封装 muise spark core 的第一目标就是简单易用,让用户以最简单的方式能够上手使用 SparkStreaming。首先我们实现了帮助用户自动读取与存储 Kafka Offset 的功能,用户无需 关心 Offset 是如何被处理的。其次我们也对 Kafka Offset 的有效性进行了校验,有的用户的 作业可能在停止了较长时间后重新运行会出现 Offset 失效的情形,我们也对此作了对应的 操作,目前的操作是将失效的 Offset 设置为当前有效的最老的 Offset。下图展现了用户基于 muise spark core 编写一个 Spark streaming 作业的简单示例,用户只需要短短几行代码即可 完成代码的初始化并创建好对应的 DStream: 默认情况下,作业每次都是基于上次存储的 Kafka Offset 继续消费,但是用户也可以自行决 定 Offset 的消费起点。下图中展示了设置消费起点的三种方式: 3.1.2 Exactly Once 的实现 如果实时作业要实现端对端的 exactly once 则需要数据源、数据处理与数据存储的三个阶段 都保证 exactly once 的语义。目前基于 Kafka Direct API 加上 Spark RDD 算子精确一次的保 证能够实现端对端的 exactly once 的语义。在数据存储阶段一般实现 exactly once 需要保证 存储的过程是幂等操作或事务操作。很多系统本身就支持了幂等操作,比如相同数据写 hdfs 大数据篇 165 同一个文件,这本身就是幂等操作,保证了多次操作最终获取的值还是相同;HBase、 ElasticSearch 与 redis 等都能够实现幂等操作。对于关系型数据库的操作一般都是能够支持 事务性操作。 官方在创建 DirectKafkaInputStream 时只需要输入消费 Kafka 的 From Offset,然后其自行获 取本次消费的 End Offset,也就是当前最新的 Offset。保存的 Offset 是本批次的 End Offset, 下次消费从上次的 End Offset 开始消费。当程序宕机或重启任务后,这其中存在一些问题。 如果在数据处理完成前存储 Offset,则可能存在作业处理数据失败与作业宕机等情况,重启 后会无法追溯上次处理的数据导致数据出现丢失。如果在数据处理完成后存储 Offset,但是 存储 Offset 过程中发生失败或作业宕机等情况,则在重启后会重复消费上次已经消费过的 数据。而且此时又无法保证重启后消费的数据与宕机前的数据量相同数据相当,这又会引入 另外一个问题,如果是基于聚合统计指标作更新操作,这会带来无法判断上次数据是否已经 更新成功。 所以在 muise spark core 中我们加入了自己的实现用以保证 Exactly once 的语义。具体的实 现是我们对 Spark 源码进行了改造,保证在创建 DirectKafkaInputStream 可以同时输入 From Offset 与 End Offset,并且我们在存储 Kafka Offset 的时候保存了每个批次的起始 Offset 与 结束 Offset,具体格式如下: 如此做的用意在于能够确保无论是宕机还是人为重启,重启后的第一个批次与重启前的最后 一个批次数据一模一样。这样的设计使得后面用户在后面对于第一个批次的数据处理非常灵 活可变,如果用户直接忽略第一个批次的数据,那此时保证的是 at most once 的语义,因为 我们无法获知重启前的最后一个批次数据操作是否有成功完成;如果用户依照原有逻辑处理 第一个批次的数据,不对其做去重操作,那此时保证的是 at least once 的语义,最终结果中 可能存在重复数据;最后如果用户想要实现 exactlyonce,muise spark core 提供了根据 topic、 partition 与 offset 生成 UID 的功能,只要确保两个批次消费的 Offset 相同,则最终生成的 UID 也相同,用户可以根据此 UID 作为判断上个批次数据是否有存储成功的依据。下面简单 的给出了重启后第一个批次操作的行为。 3.1.3 Metrics 系统 大数据篇 166 Musiespark core 基于 Spark 本身的 metrics 系统进行了改造,添加了许多定制的 metrics, 并且向用户暴露了 metrics 注册接口,用户可以非常方便的注册自己的 metrics 并在程序中 更新 metrics 的数值。最后所有的 metrics 会根据作业设定的批次间隔写入 Graphite,基于 公司定制的预警系统进行报警,前端可以通过 Grafana 展现各项 metrics 指标。 Muisespark core 本身定制的 metrics 包含以下三种: Fail,批次时间内 spark task 失败次数超过 4 次便报警,用于监控程序的运行状态 Ack,批次时间内 spark streaming 处理的数据量小 0 便报警,用于监控程序是否在正常消费 数据 Lag,批次时间内数据消费延迟大于设定值便报警 其中由于我们大部分作业开启了 Back Pressure 功能,这就导致在 Spark UI 中看到每个批次 数据都能在正常时间内消费完成,然而可能此时 kafka 中已经积压了大量数据,故每个批次 我们都会计算当前消费时间与数据本身时间的一个平均差值,如果这个差值大于批次时间, 说明本身数据消费就已经存在了延迟。 下图展现了预警系统中,基于用户自定义注册的 Metrics 以及系统定制的 Metrics 进行预警。 3.1.4 容错 其实在上面 Exactly Once 一章中已经详细的描述了 muise spark core 如何在程序宕机后能够 保证数据正确的处理。但是为了能够让 Spark Sreaming 能够长时间稳定的运行在 Yarn 集群 上,还需要添加许多配置,感兴趣的朋友可以查看:Long running Spark Streaming Jobs on YarnCluster。 除了上述容错保证之外,Muise Portal(后面会讲)也提供了对 Spark Streaming 作业定时检 测的功能。目前每过 5 分钟对当前所有数据库中状态标记为 Running 的 Spark Streaming 作 业进行状态检测,通过 Yarn 提供的 REST APIs 可以根据每个作业的 Application Id 查询作业 在 Yarn 上的状态,如果状态处于非运行状态,则会尝试重启作业。 3.2 Muise Portal 大数据篇 167 在封装完所有的 Spark Streaming 之后,我们就需要有一个平台能够管理配置作业, MuisePortal 就是这样的存在。Muise Portal 目前主要支持了 Storm 与 Spark Streaming 两类 作业,支持新建作业、Jar 包发布、作业运行与停止等一系列功能。下图展现了新建作业的 界面: SparkStreaming 作业基于 Yarn Cluster 模式运行,所有作业通过在 Muise Portal 上的 Spark 客户端提交到 Yarn 集群上运行。具体的一个作业运行流程如下图所示: 大数据篇 168 3.3 整体架构 最后这边给出一下目前携程实时平台的整体架构。 四、Spark Streaming 在携程的实践 目前 Spark Streaming 在携程的业务场景主要可以分为以下几块:ETL、实时报表统计、个性 化推荐类的营销场景以及风控与安全的应用。从抽象上来说,主要可以分为数据过滤抽取、 数据指标统计与模型算法的使用。 4.1 ETL 如今市面上有形形色色的工具可以从 Kafka 实时消费数据并进行过滤清洗最终落地到对应 的存储系统,如:Camus、Flume 等。相比较于此类产品,Spark Streaming 的优势首先在于 大数据篇 169 可以支持更为复杂的处理逻辑,其次基于 Yarn 系统的资源调度使得 Spark Streaming 的资 源配置更加灵活,最后用户可以将 Spark RDD 数据转换成 Spark Dataframe 数据,使得可以 与 Spark SQL 相结合,并且最终将数据输出到 HDFS 和 Alluxio 等分布式文件系统时可以存 储为 Parquet 之类的格式化数据,用户在后续使用 Spark SQL 处理数据时更为的简便。 目前在 ETL 使用场景中较为典型的是携程度假部门的 Data Lake 应用,度假部门使用 Spark Streaming 对数据做 ETL 操作最终将数据存储至 Alluxio,期间基于 muise-spark-core 的自定 义 metric 功能对数据的数据量、字段数、数据格式与重复数据进行了数据质量校验与监控, 具体的监控预警已在上面说过。 4.2 实时报表统计 实时报表统计与展现也是 Spark Streaming 使用较多的一个场景,数据可以基于 Process Time 统计,也可以基于 Event Time 统计。由于本身 Spark Streaming 不同批次的 job 可以视为一 个个的滚动窗口,某个独立的窗口中包含了多个时间段的数据,这使得使用 SparkStreaming 基于 Event Time 统计时存在一定的限制。一般较为常用的方式是统计每个批次中不同时间 维度的累积值并导入到外部系统,如 ES;然后在报表展现的时基于时间做二次聚合获得完 整的累加值最终求得聚合值。下图展示了携程 IBU 基于 Spark Streaming 实现的实时看板。 大数据篇 170 4.3 个性化推荐与风控安全 这两类应用的共同点莫过于它们都需要基于算法模型对用户的行为作出相对应的预测或分 类,携程目前所有模型都是基于离线数据每天定时离线训练。在引入 Spark Streaming 之后, 许多部门开始积极的尝试特征的实时提取、模型的在线训练。并且 Spark Streaming 可以很 好的与 Spark MLlib 相结合,其中最为成功的案例为信安部门以前是基于各类过滤条件抓取 攻击请求,后来他们采用离线模型训练,Spark Streaming 加 Spark MLlib 对用户进行实时预 测,性能上较 JStorm(基于大量正则表达式匹配用户,十分消耗 CPU)提高了十倍,漏报率 降低了 20%。 五、曾经踩过的坑 目前携程的 Spark Streaming 作业运行的 YARN 集群与离线作业同属一个集群,这对作业无 论是性能还是稳定性都带来了诸多影响。尤其是当 YARN 或者 Hadoop 集群需要更新维护 重启服务时,在很大程度上会导致 Spark Streaming 作业出现报错、挂掉等状况,虽然有诸 多的容错保障,但也会导致数据积压数据处理延迟。后期将会独立部署 Hadoop 与 Yarn 集 群,所有的实时作业都运行在独立的集群上,不受外部的影响,这也方便后期对于 Flink 作 业的开发与维护。后期通过 Alluxio 实现主集群与子集群间的数据共享。 在使用过程中,也遇到了形形色色不同的 Bug,这边简单的介绍几个较为严重的问题。首先 第一个问题是,Spark Streaming 每个批次 Job 都会通过 DirectKafkaInputStream 的 comput 方法获取消费的 Kafka Topic 当前最新的 offset,如果此时 kafka 集群由于某些原因不稳定, 就会导致 java.lang.RuntimeException: No leader found for partition xx 的问题,由于此段代 码运行在 Driver 端,如果没有做任何配置和处理的情况下,会导致程序直接挂掉。对应的解 决 方 法 是 配 置 spark.streaming.kafka.maxRetries 大于 1 , 并 且 可 以 通 过 配 置 refresh.leader.backoff.ms 参数设置每次重试的间隔时间。 其次在使用 Spark Streaming 与 Spark Sql 相结合的过程中,也会有诸多问题。比如在使用过 程中可能出现 out of memory:PermGen space,这是由于 Spark sql 使用 code generator 导 大数据篇 171 致 大 量 使 用 PermGen space ,通过在 spark.driver.extraJavaOptions 中添加- XX:MaxPermSize=1024m-XX:PermSize=512m 解 决 。 还 有 Spark Sql 需 要 创 建 Spark Warehouse,如果基于 Yarn 来运行,默认可能是在 HDFS 上创建相对应的目录,如果没有 权 限 会 报 出 Permission denied 的问题,用户可以通过配置 config("spark.sql.warehouse.dir","file:${system:user.dir}/spark-warehouse")来解决。 六、未来展望 上面主要针对 Spark Streaming 在携程实时平台中的运用做了详细的介绍,在使用 SparkStreaming 过程中还是存在一些痛点,比如窗口功能比较单一、基于 Event Time 统计 指标过于繁琐以及官方在新的版本中基本没有新的特性加入等,这使得我们更加倾向于尝试 Flink。Flink 基本实现了 Google 提出的各类实时处理的理念,引入了 WaterMark 的实现,感 兴趣的朋友可以查看 Google 官方文档:The world beyond batch: Streaming 102。 目前 Flink 1.4 release 版本发布在即,Spark 2.2.0 基于 kafka 数据源的 Structured Streaming 也支持了更多的特性。前期我们已对 Flink 做了充分的调研,下半年主要工作将放在 Flink 的 对接上。在提供了诸多实时计算框架的支持后,随之而来的是带来了更多的学习成本,今后 我们的重心将放在如何使用户更加容易的实现实时计算逻辑。其中 Apache Beam 对各种实 时场景提供了良好的封装并对多种实时计算引擎做了支持,其次基于 Stream Sql 实现复杂 的实时应用场景都将是我们主要调研的方向。 无线篇 172 无线篇 无线篇 173 从零打造携程无线持续交付平台 MCD 实践 [作者简介]刘李丰,携程无线基础工程团队高级经理,负责无线交付平台和基础服务研发。 十多年的互联网从业经验,曾供职于 eBay 等互联网公司,研发经验丰富。 目前携程大部分订单已来自移动端,App 几乎承载了整个集团的所有业务形态。在内部研 发中,携程的 App 已经发展成为拥有 90+ Native Bundle、100+ Hybrid Bundle、30+ React Native Bundle,几百名研发人员,每个版本( 1 个 月 ) 交 付 4000+ 个 App 包, Hybrid/ReactNative/HotFix/Bundle 发布次数 500+。 如果没有一个有效的无线持续交付平台,很难实现大版本的集成发布在 3 天内完成。而对 比市场上开源的无线持续集成工具 Fastlane、TestFlight、Jenkins 都存在各种定制化需求的 问题。因此我们从零开始,逐步打造适合携程研发流程的无线交付平台,系统化地解决研发 支撑痛点。 下面将从集成、测试、发布、运营四个子平台来展开,具体分享我们是如何一步步打造无线 持续交付平台的。 一、集成平台 从最初到现在,携程无线持续交付模型经历了从 1.0 到 2.0 的迭代演进。 在 1.0 之前,App 集成和发布还主要依靠人工操作 Jenkins,需要由特定的打包人员负责打 包,再将包通过 IM/邮件等方式传递给其他测试人员,测试结果需要专人手工回收,以把控 App 质量。此时最大的问题就是 App 管理混乱,人工介入过多,每次发布都需要花费很长 的时间。 1、1.0 阶段 在 1.0 阶段,我们引入 MCD(MobileContinues Delivery)平台思路,将打包人员的工作交 给平台来完成,提高了发布工作效率。这时不需要专职人员负责出包,平台会定时自动打包, 测试人员可以到平台上自由取包(通过下载链接或扫描二维码的方式)进行测试。 与此同时,测试人员也可以在平台上进行单独的打包和测试。测试结果会统一反馈到平台上 来,协调人员在平台根据各家的反馈结果决定需要重新出包还是继续下一步发布操作。 在这个阶段,App 的打包还完全依赖于源代码进行,由平台生成打包参数(主要包含 App 运行环境、与 iOS 签名相关的参数以及代码仓库相关的参数)提交给后台的打包系统。它 会根据仓库地址和 commit ID 从代码仓库中拉取全量代码,然后打包系统再调用代码中预 先准备好的 Build 脚本构建 App 产物,构建完成后将结果保存至临时的文件服务中,最后 由平台的回收进程将构建结果回收并处理之后放在云存储上供用户下载使用。 无线篇 174 2、2.0 阶段 1.0 阶段虽然已经基本实现了集成打包的自动化,但是还存在以下几点不足:  源码打包方式效率低下,每次都要从代码仓库中下载全量代码,再通过源代码生成 App 产物。这样做使得每次 Build 时间都很长,而打包次数的增加会导致某些打包任务积压, 系统不能及时出包。  编译容易失败,任何一个 Check-in 导致的编译失败,就会致使系统不能正常出包。  系统之间采用轮询模式,Job 任务扩展性差。 MCD 系统发起 Build 请求之后会有另一个定时的 Job 任务去轮询 Build Server 查看 Build 结果。在初期阶段还能满足业务需求,但是后来由于打包数量的增加以及打包频率的 提高,系统的处理效率变得越来越低。 一方面打包资源不够(Android 打包使用 Linux 虚拟机,iOS 打包使用 Mac),另一方面轮 询 Job 的处理效率达到了瓶颈。打包机器采用 Jenkins 方式进行管理,因此很容易进行横 向扩展,但是 Job 却很难扩容。 针对以上问题,我们对平台进行了升级改造,主要为:  引入消息机制,提高系统吞吐量;  将工程进行拆分,按照 Bundle 的方式进行打包。 消息机制的改造: 首先基本打包架构和流程保持不变,在 MCD 系统和 Build Server 之间增加消息系统,MCD 发起 Build 之后不再轮询 Build Server,而是消费由 Build Server 产生的 Build 完成消息, 如图 1 所示。使用这样的生产消费模型 MCD 可以轻易地进行水平扩展,系统执行效率得 到极大改善。 无线篇 175 图 1 Bundle 打包 工程拆分: App 工程拆分成多个不同的 Bundle 模块,Bundle 之间存在依赖关系。在这个情况下 App 的编译和打包也可以按照 Bundle 的方式进行组织。 在开发阶段,开发人员提交完代码之后就能直接将自己负责的 Bundle 编译成可执行文件, 测试可以自由选择 Bundle 进行打包。此时打包工作只需将多个 Bundle 文件组装在一起就 行,极大地加快了打包速度。通过测试的 Bundle 会被发布到指定地点,在 App 集成阶段 只需把所有发布的 Bundle 组装成最终 App 供大家测试即可。 在开发自测阶段,开发人员提交完代码后在 MCD 平台上 Build 出所开发的 Bundle(标记 为 L,表示 Latest)。相关测试人员(或开发人员自己)可以打测试包,测试包会使用所有 Bunde 的 L 版本进行构建。构建完成之后获取测试包进行功能测试,测试通过后就可以将 该 Bundle 进行发布操作,即标记为 RC 版本(表示 Release Candidate)。 图 2 测试打包发布 如图 2 所示,在集成测试阶段,MCD 平台会自动选取已发布(RC 版本)的 Bundle 打出 集成包,测试完成后测试人员可以将测试结果反馈到 MCD 平台中,待测试全部通过之后就 可以安排 App 进入发布定版阶段,再进行一些后续市场包操作就可提交给 App 应用市场 供用户下载。 由于 App 的开发是个连续过程,测试包和集成包都可以很方便地配置是需要 Hourlybuild 还是 Daily build,并且可以和测试平台的相关功能联动,从而实现持续集成。 我们也会将不同版本(不同功能)的 App 按照项目进行划分,主版 App 为标准项目,非 主版(渠道/HotFix/Bundle)App 为非标准项目,同一项目内的 Bundle 可以自由组合,项 目之间互相独立,不受影响。一个项目在创建时可以选择从另一个项目继承其各模块的最新 版和发布版。项目在开发过程中也可以从其父项目中实时同步对应模块的最新版和发布版, 无线篇 176 这样就能基本满足各种开发和测试的需求。 二、测试平台 随着携程持续交付的发展,原有的测试模式以及流程渐渐显现出不足,主要体现在以下三点: 快速验证与手工验证的冲突; 自动化回归测试与手动测试效率的矛盾; 设备兼容测试与设备不足的矛盾。 1、快速验证与白屏检测 在集成测试阶段,每一次出包首先会由基础测试人员负责验证此次出包的质量,主要是验证 各个 BU 入口页面 Hybrid 或直连页面是否正常,如果大量 BU 入口页面异常则会要求重 新出包。这个过程看似很简单,但是“(沟通成本+验证成本)*出包次数”所耗费的时间也是 不容忽视的,且这种冒烟测试对于测试人员也很枯燥无趣。 图 3 集成包 针对以上情况,最初引入了白屏测试等基础性测试功能,并与集成平台打通(见图 3,可以 直接从集成包列表选择相关测试功能)。 白屏测试主要是检测 App 首页各个入口页面是否显示正常,对于每一个页面会进行图像比 对以及页面长度来验证页面是否正常,白屏会在多个设备进行测试,只要其中一台设备通过, 则认为这个入口是正常的。 2、自动化回归测试 通过回归测试自动化,可以提高回归测试效率,有效缓解测试人员的压力。我们逐步开发了 MCD 测试平台用于自动化测试项目的管理、执行、报告和展示,支持 Android/iOS App、 HTML5/Hybrid Testing。 无线篇 177 图 4 测试平台架构 测试平台架构简化图如图 4 所示,各个模块主要功能如下:  Portal 负责与用户交互,查看报表,保持后台系统对用户透明;  Invoker 是调度通道,用户基于 Portal 提交的测试任务通过此通道来调度,包括创建项 目、设备占用、测试项目执行等;  Lab Center 负责所有与设备相关的业务,Lab Center 与二次开发的开源软件 STF 交互, 所有设备挂载在 STF 上;  当具体的测试项目在 Jenkins 上执行时,会将预先占用的设备远程连接到 Slave 上等 待使用。 MCD 测试平台提供了自动化测试(白屏检测、录制回放、回归测试)、系统测试(兼容性、 性能、稳定性)、手动测试(远程租用、远程调试)三大类功能,基本达到了我们对于提高 测试效率的需求,如图 5、图 6 所示。 无线篇 178 图 5 Mobile 项目列表 图 6 自动化测试 Task Case 页面 另外,日常测试中还经常遇到这些情况:需要快速验证 App 的一个功能,有些问题只能在 特定机型上重现,测试目前却没有这款手机。因此我们基于 STF 二次开发出设备共享平台, 将空闲设备收集起来在平台上共享,用户只需在平台上搜索需要的设备就可以立刻通过 STF 进行使用了,如图 7 所示。 无线篇 179 图 7 设备租用 同时,我们还建立了代码质量的自动化,集成 Sonar、Facebook Infer 和 Uint Test 功能, 方便每个版本对代码质量进行跟踪,如图 8 所示。 图 8 代码质量自动化追踪 三、发布平台 无线发布系统一直以来都是无线应用能快速迭代最关键的一环,一个好的发布系统能帮助开 发快速地修复线上问题,并能快速将新功能投送至用户,帮助业务抢占市场,而发布系统的 设计也随着 CD(Continuous Delivery)概念的深入人心而日新月异。 无线发布的特点是静态资源包(不管是 Hybrid、React Native 还是 HotFix、Bundle),发布 技术中涉及到的主要问题如下:  如何最大限度地把静态资源的下载流量降到最低,以减少用户对应用更新的感知度,提 高应用升级的成功率和用户体验。  灰度发布,保证发布的质量。 1、资源包发布 针对静态资源包发布,经历了如下几个历程:  全量包发布: 携程最初使用的都是全量发布的方式,这种方式实现简单,但也简单暴 无线篇 180 力,而问题也比较明显,导致用户升级时流量巨大。  文件字符串差分: 这种方法避免了整包差分的问题,能够比较方便地实现快速迭代要 求下的小量更新需求,并且文本差分工具简单,实现容易,出错较少,但缺点是差分效 果差。  文件二进制差分: 对每个文件进行二进制级别的差分能够具备小量快跑的特点,同时 进一步优化差分文件 Size。这也是携程目前使用的方式,它仍然使用了 bsdiff 作为差 分工具,当文件没有更新时,不输出差分文件,而更新时也只产生 Size 非常小的 diff 文件。  按需差分: 在按需差分前,我们采取的是预差分方案。就是在发布时,把所有的版本 间差分准备好,有多少个历史版本就将产生多少个差分包。按照这种做法,随着发布次 数的增多,发布差分包数量也将急剧增长,对存储的要求越来越高,也造成了浪费。 在此前提下,引入了按需差分,就是终端用户通过将自己的版本号与线上最新版本号做比较, 发现落后时,触发差分过程,从而获取增量更新。 这里会有一个问题——终端用户第一次访问时没有差分包,我们的做法是:第一次只返回全 量包,用户不用等待,但是后续用户会拿到差分包。 另外一个问题是,避免重复打差分包。在计算时发现无差分包,会触发差分包过程,同时会 把这个信息缓存(过期时间半个小时)。当其他客户端同样的版本请求时,会先判断缓存, 没有的话进行差分操作,有的话直接下发全量包。 2、灰度发布 网站发布通常是有灰度发布的过程,无线的发布跟网站发布流程是类似的,但是做法有差别。 二者的区别是:网站是 centralize 的,可以统一控制,而无线的发布是 decentralized。无线 发布包,像 Hybrid、React Native、HotFix 等静态资源,是需要客户端一个个来拉取的。那 么,如何确保整个发布的流畅,确保发布质量呢?我们的解决方案如下: Offline 灰度发布流程 灰度第一步是冒烟,网站的冒烟是导一部分流量到某台 Server 上,而我们则是通过配置一 个白名单,只有客户端在白名单中的才会获取到最新发布。当测试没有通过,可以选择终止, 然后再决定是否需要回滚,测试通过决定是否需要扩大比例。 监控发布数据 当客户端收到下载物后,会把当前的下载信息上报上来,供发布人员进行监控查看。 灰度发布设计中需要注意:  避免一直小白鼠:为了避免一个用户一直当小白鼠,每次灰度需要利用灰度 ID+客户端 信息进行取模运算。  灰度回滚:灰度回滚时只需要回滚已经更新这次灰度的客户端,对于没有更新到的,则 无线篇 181 无需回滚。 四、运营平台 运营是研发流程的最后一环,携程的 MCD 平台思路是能够针对发布结果、运营情况以及配 置等功能有一个集中的管理。目前包含了运营看板、性能看板、发布看板、无线配置、崩溃 管理、App Size 统计等模块。 其中无线配置经历了多次迭代,对于各种配置项,从版本、平台、渠道、A/B 多个角度进行 了定义,做到灵活动态配置,需改就改,粒度大小自由配置。下发时根据 App 的版本、平 台、渠道等信息挑选符合条件的配置进行发布。需要修改的时候也是先修改测试环境配置, 测试通过后再同步到生产。配置分为专用版和通用版两种,专用版只会下发给特定的渠道/ 版本,通用版以版本号为基准,在不发布新版本配置的情况下系统默认给 App 端下发最近 的通用版配置。当同时存在符合条件的专用版和通用版的时候,只下发专用版配置。 崩溃管理也是无线运营的重要一环,尽管目前行业内已经有崩溃管理功能强大的平台,但由 于定制性需求我们也搭建了自有崩溃管理功能,方便快速发现线上问题,从而能够通过 HotFix/Bundle 方式及时修复,如图 9 所示。 图 9 崩溃管理页面 五、结束语 以上是携程无线基础工程团队在无线持续交付方面的一些工程实践,MCD 的核心目标是能 够对集成、测试、发布和运营的完整生命周期提供更好的平台支撑和管理。虽然取得了一些 成果,但还有许多提升空间,例如多应用支持、支持条件发布等,无线持续发布之路依然有 很多工作要做。 无线篇 182 携程小程序开发的那些事儿 [作者简介]朱天超,携程技术中心基础业务研发部 iOS 开发工程师,从事一线 iOS 开发,偶 尔也写写脚本做些工具。 小程序是一种不需要下载安装即可使用的应用。它实现了应用“触手可及”的梦想,用户扫 一扫或者搜一下即可打开应用;也体现了“用完即走”的理念,用户不用关心是否安装太多 应用的问题。 早在 2016 年 9 月,携程就获得小程序内测名额,在对微信文档规则调研、确定需要开发的 业务之后,就开始了风风火火的开发。 回顾三个多月的开发过程,其中碰到了各种问题,也尝试了一些解决办法,最终打造了携程 独立的小程序框架。 本文将主要介绍携程小程序的框架,开发和发布审核中碰到的问题及我们的一些解决办法。 一、携程微信小程序的框架介绍 1、CPage() CPage 是封装了微信的 Page 的函数,内部通过插入中间件装饰页面初始化的 option,主要 实现页面间通信、页面层级控制、埋点、页面分享等功能。 中间件  Base : 装饰 option  Navigator:内部维护 navigatorOpts 存储上文的参数回调等,pageStack 存储页面栈信 息  UBT :为业务提供埋点分析、统计页面驻留等信息分享 分享  在 CPage 中特殊处理,同时也方便统计分析 CPage 中 data 处理的具体流程如下图: 无线篇 183 流程解释: 1) data 传入 CPage 2) Baes 同步 data 生命周期,接受 data 自有属性 3) Navigator 控制层级,使用 navigateTo 上下文交互 4) UBT 提供统计分析功能,及后续更多中间件处理 5) newData 接受 data.onShareAppMessage 和中间件的属性 6) 传入原生 Page() 2、CWX cwx 的原型是 wx,扩展了更多 API,工具类、网络请求相关、定位相关、组件 API 等。 网络请求 wx.request()最大并发数是 5,所以要避免同时并发很多请求。 主要在两个方面: 1、cwx 提供了 request,内部通过队列的方式控制 request,并且提供了 cancel 方法取消在 等待队列中的 request 2、服务端数据合并,小程序端单个页面请求减少 无线篇 184 工具类 提供常用的方法,如 Base64 加解密,JS 对象类型的判断等 定位 1、使用 wx.getLocation 获取到经纬度,上传服务端匹配定位城市相关信息,使用百度逆地 址解析 2、内部实现队列控制定位请求,解决授权等问题 组件 API 对接组件,提供快捷使用组件的 API,可在任意页面使用基础组件 cwx.component.calendar() cwx.component.city() cwx.component.imagebrowser() 3、基础组件 基础组件都是基于 CPage 实现的,参数传递页面回调都遵循 CPage 的规则  日历组件 提供常用的日期选择功能,日期元素支持一级标题、二级标题及颜色等自定义  城市选择组件 支持国内和国际城市选择,支持索引筛选,提供搜索功能  图片浏览组件 提供图片大图浏览支持标题和描述,如果有层级约束可使用 wx.previewImage 组件,建 议直接使用 wx.previewImage,有三点好处:1、不占层级;2、支持分享;3、性能好。 组件效果图如下: 无线篇 185 二、开发过程中的问题及解决办法 1、渲染白屏 一次性渲染几百条数据,会造成页面长时间白屏,即便使用 wx.showToast 也无法规避体验 较差的问题,可考虑一下几个方面解决问题: 1) 局部渲染:如果页面有明确的区块,同时数据也是隔离的,可优先在 onLoad 中渲染一部 分,数据量较大的推迟到 onReady 中渲染 2) 列表渲染:设计为分页模式,在 onReachBottom 中追加下一页数据;使用 scroll-view 监 听 bindscrolltolower 自动追加下一页数据 3) 设计规避:页面设计时考虑渲染问题,采用其他交互方式规避 2、页面层级 小程序规定页面层级最多不超过 5 层,可考虑一下解决办法: 1) 业务交互上规避:超过 5 层时小程序端是无能为力的,只能在业务交互设计时避免 无线篇 186 2) 使用浮层:能使用浮层取代的 Page 的,优先考虑浮层 3) wx.redirectTo:某些情况下可使用重定向 Promise IDE 在 0.11.112200 版本移除了 Promise,继续使用可自行引入 es6-promise-min 3、授权弹框 在使用定位和获取用户信息 API 时,微信客户端会弹出授权框,如果用户未授权,并且同一 时刻多次调用授权 API,会造成多个授权框叠加的情况。 解决方法:对需要授权的 API 进行封装,统一控制权限,避免在未得到授权的情况下多次调 用授权 API,如 cwx.locate 内部使用队列控制定位请求。 4、Package Size 小程序规定上传的包大小不得超过 1M,代码层面可参考以下方法减少 size: 1) IDE 控制台提示的 warn 尽量解决:wx:key 2) 将 wxml 中的逻辑判断迁移到 js 中 3) wxss 中尽量少用 import,将大的 wxss 根据需要拆分成多个小的 wxss import 4) 调试代码删除 5) 图片资源尽量在线 6) 代码压缩 5、异常报错 使用微信 IDE 开发过程会碰到很多问题,常用的解决办法: 1) 代码写好一定要 ctrl+s 2) 回退代码排错 3) 逐行删代码排错 4) 清理缓存 5) 重启 IDE 无线篇 187 三、审核遇到的问题 1、审核中的版本无法撤销 如果提交审核就不能撤销,只能等待微信的审核结果,必须要注意。 2、提交新的版本会将已经审核通过未发布的版本覆盖 小程序审核流程并不像 AppStore 可以同时发布多个版本,不确定微信后续会不会修复。 3、提交页面和类目不匹配被拒 填写页面信息时,注册的页面需要匹配小程序的类目,并且每个类目只能注册一个页面。 4、体验问题被拒 微信对交互体验审核比较严格,如果交互有明显的问题或者逻辑不合理会被拒绝。 四、后续的发展规划 为了方便后续更多小程序的开发,我们还会将主版本框架抽取出来,做成可配置的独立的 SDK;以及根据小程序的需求,接入集成发布,动态打出不同类型的包,方便各种渠道。 前端篇 188 前端篇 前端篇 189 那些你不知道的爬虫反爬虫套路 [作者简介]崔广宇,携程酒店研发部开发经理,与去哪儿艺龙的反爬虫同事是好基友。携程 技术中心“非著名”段子手。 前言 爬虫与反爬虫,是一个很不阳光的行业。 这里说的不阳光,有两个含义。 第一是,这个行业是隐藏在地下的,一般很少被曝光出来。很多公司对外都不会宣称自己有 爬虫团队,甚至隐瞒自己有反爬虫团队的事实。这可能是出于公司战略角度来看的,与技术 无关。 第二是,这个行业并不是一个很积极向上的行业。很多人在这个行业摸爬滚打了多年,积攒 了大量的经验,但是悲哀的发现,这些经验很难兑换成闪光的简历。面试的时候,因为双方 爬虫理念或者反爬虫理念不同,也很可能互不认可,影响自己的求职之路。本来程序员就有 “文人相轻”的倾向,何况理念真的大不同。 然而这就是程序员的宿命。不管这个行业有多么的不阳光,依然无法阻挡大量的人进入这个 行业,因为有公司的需求。 那么,公司到底有什么样的需求,导致了我们真的需要爬虫/反爬虫呢? 反爬虫很好理解,有了爬虫我们自然要反爬虫。对于程序员来说,哪怕仅仅是出于“我就是 要证明我技术比你好”的目的,也会去做。对于公司来说,意义更加重大,最少,也能降低 服务器负载,光凭这一点,反爬虫就有充足的生存价值。 那么爬虫呢? 最早的爬虫起源于搜索引擎。搜索引擎是善意的爬虫,可以检索你的一切信息,并提供给其 他用户访问。为此他们还专门定义了 robots.txt 文件,作为君子协定,这是一个双赢的局面。 然而事情很快被一些人破坏了。爬虫很快就变的不再“君子”了。 后来有了“大数据”。无数的媒体鼓吹大数据是未来的趋势,吸引了一批又一批的炮灰去创 办大数据公司。这些人手头根本没有大数据,他们的数据只要用一个 U 盘就可以装的下,怎 么好意思叫大数据呢?这么点数据根本忽悠不了投资者。于是他们开始写爬虫,拼命地爬取 各个公司的数据。很快他们的数据,就无法用一个 U 盘装下了。这个时候终于可以休息休 息,然后出去吹嘘融资啦。 前端篇 190 然而可悲的是,大容量 U 盘不断地在发布。他们总是在拼命地追赶存储增加的速度。L 以上是爬虫与反爬虫的历史。 一、爬虫反爬虫运行现状 电子商务行业的爬虫与反爬虫更有趣一些,最初的爬虫需求来源于比价。 这是某些电商网站的核心业务。大家如果买商品的时候,是一个价格敏感型用户的话,很可 能用过网上的比价功能(真心很好用啊)。毫无悬念,他们会使用爬虫技术来爬取所有相关电 商的价格。他们的爬虫还是比较温柔的,对大家的服务器不会造成太大的压力。 然而,这并不意味着大家喜欢被他爬取。毕竟这对其他电商是不利的。于是需要通过技术手 段来做反爬虫。 按照技术人员的想法,对方用技术怼过来,我们就要用技术怼回去,不能怂啊。这个想法是 很好的,但是实际应用起来根本不是这么回事。 诚然,技术是很重要的,但是实际操作上,更重要的是套路。谁的套路更深,谁就能玩弄对 方于鼓掌之中。谁的套路不行,有再好的技术,也只能被耍的团团转。这个虽然有点伤技术 人员的自尊,然而,我们也不是第一天被伤自尊了。大家应该早就习惯了吧。 1、真实世界的爬虫比例 大家应该听过一句话吧,大概意思是说,整个互联网上大概有 50%以上的流量其实是爬虫。 第一次听这句话的时候,我还不是很相信,我觉得这个说法实在是太夸张了。怎么可能爬虫 比人还多呢? 爬虫毕竟只是个辅助而已。 现在做了这么久的反爬虫,我依然觉得这句话太夸张了。50%?你在逗我?就这么少的量? 举个例子,某公司,某个页面的接口,每分钟访问量是 1.2 万左右。这里面有多少是正常用 户呢? 50%?60%?还是? 正确答案是:500 以下。 也就是说,一个单独的页面,12000 的访问量里,有 500 是正常用户,其余是爬虫。 注意,统计爬虫的时候,考虑到你不可能识别出所有的爬虫,因此,这 500 个用户里面,其 实还隐藏着一些爬虫。那么爬虫率大概是: (12000-500)/12000=95.8% 前端篇 191 这个数字你猜到了吗? 这么大的爬虫量,这么少的用户量,大家到底是在干什么?是什么原因导致了明明是百人级 别的生意,却需要万级别的爬虫来做辅助? 95%以上,19 保 1? 答案可能会相当令人喷饭。这些爬虫大部分是由于决策失误导致的。 2、哭笑不得的决策思路 举个例子,这个世界存在 3 家公司,售卖相同的电商产品。三家公司的名字分别是 A,B, C。 这个时候,客户去 A 公司查询了下某商品的价格,看了下发现价格不好。于是他不打算买 了。他对整个行业的订单贡献为 0。 然而 A 公司的后台会检测到,我们有个客户流失了,原因是他来查询了一个商品,这个商品 我们的价格不好。没关系,我去爬爬别人试试。 于是他分别爬取了 B 公司和 C 公司。 B 公司的后台检测到有人来查询价格,但是呢,最终没有下单。他会认为,嗯,我们流失了 一个客户。怎么办呢? 我可以爬爬看,别人什么价格。于是他爬取了 A 和 C。 C 公司的后台检测到有人来查询价格。。。。。 过了一段时间,三家公司的服务器分别报警,访问量过高。三家公司的 CTO 也很纳闷,没 有生成任何订单啊,怎么访问量这么高? 一定是其他两家禽兽写的爬虫没有限制好频率。 妈的,老子要报仇。于是分别做反爬虫,不让对方抓自己的数据。然后进一步强化自己的爬 虫团队抓别人的数据。一定要做到:宁叫我抓天下人,休叫天下人抓我。 然后,做反爬虫的就要加班天天研究如何拦截爬虫。做爬虫的被拦截了,就要天天研究如何 破解反爬虫策略。大家就这么把资源全都浪费在没用的地方了。直到大家合并了,才会心平 气和的坐下来谈谈,都少抓点。 最近国内的公司有大量的合并,我猜这种“心平气和”应该不少吧? 二、爬虫反爬虫技术现状 下面我们谈谈,爬虫和反爬虫分别都是怎么做的。 1、为 python 平反 前端篇 192 首先是爬虫。爬虫教程你到处都可以搜的到,大部分是 python 写的。我曾经在一篇文章提 到过:用 python 写的爬虫是最薄弱的,因为天生并不适合破解反爬虫逻辑,因为反爬虫都 是用 javascript 来处理。然而慢慢的,我发现这个理解有点问题(当然我如果说我当时是出 于工作需要而有意黑 python 你们信吗。。。)。 Python 的确不适合写反爬虫逻辑,但是 python 是一门胶水语言,他适合捆绑任何一种框架。 而反爬虫策略经常会变化的翻天覆地,需要对代码进行大刀阔斧的重构,甚至重写。这种情 况下,python 不失为一种合适的解决方案。 举个例子,你之前是用 selenium 爬取对方的站点,后来你发现自己被封了,而且封锁方式 十分隐蔽,完全搞不清到底是如何封的,你会怎么办?你会跟踪 selenium 的源码来找到出 错的地方吗? 你不会。你只会换个框架,用另一种方式来爬取。然后你就把两个框架都浅尝辄止地用了下, 一个都没有深入研究过。因为没等你研究好,也许人家又换方式了。你不得不再找个框架来 爬取。毕竟,老板等着明天早上开会要数据呢。老板一般都是早上八九点开会,所以你七点 之前必须搞定。等你厌倦了,打算换个工作的时候,简历上又只能写“了解n个框架的使用”, 仅此而已。 这就是爬虫工程师的宿命,爬虫工程师比外包还可怜。外包虽然不容易积累技术,但是好歹 有正常上下班时间,爬虫工程师连这个权利都没有。 然而反爬虫工程师就不可怜了吗?也不是的。反爬虫有个天生的死穴,就是:误伤率。 2、无法绕开的误伤率 我们首先谈谈,面对对方的爬虫,你的第一反应是什么? 如果限定时间的话,大部分人给我的答案都是:封杀对方的 IP。 然而,问题就出在,IP 不是每人一个的。大的公司有出口 IP,ISP 有的时候会劫持流量让你 们走代理,有的人天生喜欢挂代理,有的人为了翻墙 24 小时挂 vpn,最坑的是,现在是移 动互联网时代,你如果封了一个 IP?不好意思,这是中国联通的 4G 网络,5 分钟之前还是 别人,5 分钟之后就换人了哦! 因此,封 IP 的误伤指数最高。并且,效果又是最差的。因为现在即使是最菜的新手,也知 道用代理池了。你们可以去淘宝看下,几十万的代理价值多少钱。我们就不谈到处都有的免 费代理了。 也有人说:我可以扫描对方端口,如果开放了代理端口,那就意味着是个代理,我就可以封 杀了呀。 事实是残酷的。我曾经封杀过一个 IP,因为他开放了一个代理端口,而且是个很小众的代理 端口。不出一天就有人来报事件,说我们一个分公司被拦截了。我一查 IP,还真是我封的 IP。 前端篇 193 我就很郁闷地问他们 IT,开这个端口干什么?他说做邮件服务器啊。我说为啥要用这么奇怪 的端口?他说,这不是怕别人猜出来么?我就随便取了个。 扫描端口的进阶版,还有一种方式,就是去订单库查找这个 IP 是否下过订单,如果没有, 那么就是安全的。如果有,那就不安全。有很多网站会使用这个方法。然而这其实只是一种 自欺欺人的办法而已。只需要下一单,就可以永久洗白自己的 IP,天下还有比这更便宜的生 意吗? 因此,封 IP,以及封 IP 的进阶版:扫描端口再封 IP,都是没用的。根本不要考虑从 IP 下手, 因为对手会用大量的时间考虑如何躲避 IP 封锁,你干嘛和人家硬刚呢。这没有任何意义。 那么,下一步你会考虑到什么? 很多站点的工程师会考虑:既然没办法阻止对方,那我就让它变的不可读吧。我会用图片来 渲染关键信息,比如价格。这样,人眼可见,机器识别不出来。 这个想法曾经是正确的,然而,坑爹的技术发展,带给我们一个坑爹的技术,叫机器学习。 顺便带动了一个行业的迅猛发展,叫 OCR。很快,识别图像就不再是任何难题了。甚至连人 眼都很难识别的验证码,有的 OCR 都能搞定,比我肉眼识别率都高。更何况,现在有了打 码平台,用资本都可以搞定,都不需要技术。 那么,下一步你会考虑什么? 这个时候,后端工程师已经没有太多的办法可以搞了。 不过后端搞不定的事情,一般都推给前端啊,前端从来都是后端搞不定问题时的背锅侠。 多少年来我们都是这么过来的。前端工程师这个时候就要勇敢地站出来了: “都不要得瑟了,来比比谁的前端知识牛逼,你牛逼我就让你爬。” 我不知道这篇文章的读者里有多少前端工程师,我只是想顺便提一下:你们以后将会是更加 抢手的人才。 3、前端工程师的逆袭 我们知道,一个数据要显示到前端,不仅仅是后端输出就完事了,前端要做大量的事情, 比 如取到 json 之后,至少要用 template 转成 html 吧? 这已经是步骤最少最简单的了。然后 你总要用 css 渲染下吧? 这也不是什么难事。 等等,你还记得自己第一次做这个事情的时候的经历吗?真的,不是什么难事吗? 有没有经历过,一个 html 标签拼错,或者没有闭合,导致页面错乱?一个 css 没弄好,导 致整个页面都不知道飘到哪去了? 前端篇 194 这些事情,你是不是很想让别人再经历一次? 这件事情充分说明了:让一个资深的前端工程师来把事情搞复杂一点,对方如果配备了资深 前端工程师来破解,也需要耗费 3 倍以上的时间。毕竟是读别人的代码,别人写代码用了一 分钟,你总是要读两分钟,然后骂一分钟吧?这已经算很少的了。如果对方没有配备前端工 程师。。。那么经过一段时间,他们会成长为前端工程师。 之后,由于前端工程师的待遇比爬虫工程师稍好一些,他们很快会离职做前端,既缓解了前 端人才缺口,又可以让对方缺人,重招。而他们一般是招后端做爬虫,这些人需要再接受一 次折磨,再次成长为前端工程师。这不是很好的事情吗。 所以,如果你手下的爬虫工程师离职率很高,请仔细思考下,是不是自己的招聘方向有问题。 那么前端最坑爹的技术是什么呢?前端最坑爹的,也是最强大的,就是我们的:javascript。 Javascript 有大量的花样可以玩,毫不夸张的说,一周换一个 feature(bug)给对方学习,一年 不带重样的。这个时候你就相当于一个面试官,对方要通过你的面试才行。 举个例子,Array.prototype 里,有没有 map 啊?什么时候有啊?你说你是 xx 浏览器,那你 这个应该是有还是应该没有啊?你说这个可以有啊?可是这个真没有啊。那[]能不能在 string 里面获取字符啊?哪个浏览器可以哪个不行啊?咦你为什么支持 webkit 前缀啊?等 等,刚刚你还支持怎么现在不支持了啊?你声明的不对啊。 这些对于前端都是简单的知识,已经习以为常了。但是对于后端来说简直就是噩梦。 然而,前端人员自己作死,研究出了一个东西,叫:nodejs。基于 v8,秒杀所有的 js 运行。 不过 nodejs 实现了大量的 feature,都是浏览器不存在的。你随随便便访问一些东西(比如 你为什么会支持 process.exit),都会把 node 坑的好惨好惨。而且。。。浏览器里的 js,你拉 到后台用 nodejs 跑,你是不是想到了什么安全漏洞?这个是不是叫,代码与数据混合?如 果他在 js 里跑点恶心的代码,浏览器不支持但是 node 支持怎么办? 还好,爬虫工程师还有 phantomjs。但是,你怎么没有定位啊? 哈哈,你终于模拟出了定 位,但是不对啊,根据我当前设置的安全策略你现在不应该能定位啊?你是怎么定出来的? 连 phantomjs 的作者自己都维护不下去了,你真的愿意继续用吗? 当然了,最终,所有的反爬虫策略都逃不脱被破解的命运。但是这需要时间,反爬虫需要做 的就是频繁发布,拖垮对方。如果对方两天可以破解你的系统,你就一天一发布,那么你就 是安全的。这个系统甚至可以改名叫做“每天一道反爬题,轻轻松松学前端”。 4、误伤,还是误伤 这又回到了我们开始提到的“误伤率”的问题了。我们知道,发布越频繁,出问题的概率越 高。那么,如何在频繁发布的情况下,还能做到少出问题呢? 前端篇 195 此外还有一个问题,我们写了大量的“不可读代码”给对方,的确能给对方造成大量的压力, 但是,这些代码我们自己也要维护啊。如果有一天忽然说,没人爬我们了,你们把代码下线 掉吧。这个时候写代码的人已经不在了,你们怎么知道如何下线这些代码呢? 这两个问题我暂时不能公布我们的做法,但是大家都是聪明人,应该都是有自己的方案的, 软件行业之所以忙的不得了,无非就是在折腾两件事,一个是如何将代码拆分开,一个是如 何将代码合并起来。 关于误伤率,我只提一个小的 tip:你可以只开启反爬虫,但是不拦截,先放着,发统计信 息给自己,相当于模拟演练。等统计的差不多了,发现真的开启了也不会有什么问题,那就 开启拦截或者开启造假。 这里就引发了一个问题,往往一个公司的各个频道,爬取难度是不一样的。原因就是,误伤 检测这种东西与业务相关,公司的基础部门很难做出通用的。只能各个部门自己做。甚至有 的部门做了有的没做。因此引发了爬虫界一个奇葩的通用做法:如果 PC 页面爬不到, 就 去 H5 试试。如果 H5 很麻烦,就去 PC 碰碰运气。 三、爬虫反爬虫套路现状 那么一旦有发现对方数据造假怎么办? 早期的时候,大家都是要抽查数据,通过数据来检测对方是否有造假。这个需要人工核对, 成本非常高。可是那已经是洪荒时代的事情了。如果你们公司还在通过这种方式来检测,说 明你们的技术还比较落伍。 之前我们的竞争对手是这么干的:他们会抓取我们两次,一次是他们解密出来 key 之后,用 正经方式来抓取,这次的结果定为 A。一次是不带 key,直接来抓,这次的结果定为 B。根 据前文描述,我们可以知道,B 一定是错误的。那么如果 A 与 B 相等,说明自己中招了。这 个时候会停掉爬虫,重新破解。 1、不要回应 所以之前有一篇关于爬虫的文章,说如何破解我们的。一直有人要我回复下。我一直觉得没 什么可以回复的。 第一,反爬虫被破解了是正常的。这个世界上有个万能的爬虫手段,叫“人肉爬虫”。假设 我们就是有钱,在印度开个分公司,每天雇便宜的劳动力用鼠标直接来点,你能拿我怎么办? 第二,我们真正关心的是后续的这些套路。而我读了那篇文章,发现只是调用了 selenium 并 且拿到了结果,就认为自己成功了。 我相信你读到这里,应该已经明白为什么我不愿意回复了。我们最重要的是工作,而不是谁 打谁的脸。大家如果经常混技术社区就会发现,每天热衷于打别人脸的,一般技术都不是很 好。 前端篇 196 当然这并不代表我们技术天下第一什么的。我们每天面对大量的爬虫,还是遇到过很多高手 的。就如同武侠小说里一样,高手一般都比较低调,他们默默地拿走数据,很难被发现,而 且频率极低,不会影响我们的考评。你们应该明白,这是智商与情商兼具的高手了。 我们还碰到拉走我们 js,砍掉无用的部分直接解出 key,相当高效不拖泥带水的爬虫,一点 废请求都没有(相比某些爬虫教程,总是教你多访问写没用的 url 免得被发现,真的不知道 高到哪里去了。这样做除了会导致机器报警,导致对方加班封锁以外,对你自己没有任何好 处)。 而我们能发现这一点仅仅是是因为他低调地写了一篇博客,通篇只介绍技术,没有提任何没 用的东西。 这里我只是顺便发了点小牢骚,就是希望后续不要总是有人让我回应一些关于爬虫的文章。 线下我认识很多爬虫工程师,水平真的很好,也真的很低调(不然你以为我是怎么知道如何 对付爬虫的。。。),大家都是一起混的,不会产生“一定要互相打脸”的情绪。 顺便打个小广告,如果你对这个行业有兴趣,可以考虑联系 HR 加入我们哦。反爬虫工程师 可以加入携程,爬虫工程师可以加入去哪儿。 2、进化 早期我们和竞争对手打的时候,双方的技术都比较初级。后来慢慢的,爬虫在升级,反爬虫 也在升级。这个我们称为“进化”。我们曾经给对方放过水,来试图拖慢他们的进化速度。 然而,效果不是特别理想。爬虫是否进化,取决于爬虫工程师自己的 KPI,而不是反爬虫的 进化速度。 后期打到白热化的时候,用的技术越来越匪夷所思。举个例子,很多人会提,做反爬虫会用 到 canvas 指纹,并认为是最高境界。其实这个东西对于反爬虫来说也只是个辅助,canvas 指纹的含义是,因为不同硬件对 canvas 支持不同,因此你只要画一个很复杂的 canvas,那 么得出的 image,总是存在像素级别的误差。考虑到爬虫代码都是统一的,就算起 selenium, 也是 ghost 的,因此指纹一般都是一致的,因此绕过几率非常低。 但是!这个东西天生有两个缺陷。第一是,无法验证合法性。当然了,你可以用非对称加密 来保证合法,但是这个并不靠谱。其次,canvas 的冲突概率非常高,远远不是作者宣称的那 样,冲突率极低。也许在国外冲突是比较低,因为国外的语言比较多。但是国内公司通常是 IT 统一装机,无论是软件还是硬件都惊人的一致。我们测试 canvas 指纹的时候,在携程内 部随便找了 20 多台机器,得出的指纹都完全一样,一丁点差别都没有。因此,有些“高级 技巧”其实一点都不实用。 3、法律途径 此外就是大家可能都考虑过的:爬虫违法吗?能起诉对方让对方不爬吗?法务给的答案到是 很干脆,可以,前提是证据。遗憾的是,这个世界上大部分的爬虫爬取数据是不会公布到自 前端篇 197 己网站的,只是用于自己的数据分析。因此,即使有一些关于爬虫的官司做为先例,并且已 经打完了,依然对我们没有任何帮助。反爬虫,在对方足够低调的情况下,注定还是个技术 活。 4、搞事情,立 Flag 到了后来,我们已经不再局限于打打技术了。反爬虫的代码里我们经常埋点小彩蛋给对方, 比如写点注释给对方。双方通过互相交战,频繁发布,居然聊的挺 high 的。 比如问问对方,北京房价是不是很高啊?对方回应,欧巴,我可是凭本事吃饭哦。继续问, 摇到号了吗?诸如此类等等。这样的事情你来我往的,很容易动摇对方的军心,还是很有作 用的。试想一下,如果你的爬虫工程师在大年三十还苦逼加班的时候,看到对方留言说自己 拿到了 n 个月的年终奖,你觉得你的工程师,离辞职还远吗? 最后,我们终于搞出了大动作,觉得一定可以坑对方很久了。我们还特意去一家小火锅店吃 了一顿,庆祝一下,准备明天上线。大家都知道,一般立 flag 的下场都比较惨的。两个小时 的自助火锅,我们刚吃五分钟,就得到了我们投资竞争对手的消息。后面的一个多小时,团 队气氛都很尴尬,谁也说不出什么话。我们组有个实习生,后来鼓足勇气问了我一个问题: “我还能留下来吗?” 毕竟,大部分情况下,技术还是要屈服于资本的力量。 四、爬虫反爬虫的未来 与竞争对手和解之后,我们去拜访对方,大家坐在了一起。之前网上自称妹子的,一个个都 是五大三粗的汉子,这让我们相当绝望,在场唯一的一个妹子还是我们自己带过去的(就是 上面提到的实习生),感觉套路了这么久,最终还是被对方套路了。 好在,吃的喝的都很好,大家玩的还是比较 high 的。后续就是和平年代啦,大家不打仗了, 反爬虫的逻辑扔在那做个防御,然后就开放白名单允许对方爬取了。群里经常叫的就是:xxx 你怎么频率这么高,xxx 你为什么这个接口没给我开放,为什么我爬的东西不对我靠你是不 是把我封了啊。诸如此类的。 和平年代的反爬虫比战争年代还难做。因为战争年代,误伤率只要不是太高,公司就可以接 受。和平年代大家不能搞事情,误伤率稍稍多一点,就会有人叫:好好的不赚钱,瞎搞什么 搞。此外,战争年代只要不拦截用户,就不算误伤。和平年代还要考虑白名单,拦截了合作 伙伴也是误伤。因此各方面会更保守一些。不过,总体来说还是和平年代比较 happy。毕竟, 谁会喜欢没事加班玩呢。 然而和平持续的不是很久,很快就有了新的竞争对手选择爬虫来与我们打。毕竟,这是一个 利益驱使的世界。只要有大量的利润,资本家就会杀人放火,这不是我们这些技术人员可以 决定的。我们希望天下无虫,但是我们又有什么权利呢。 前端篇 198 好在,这样可以催生更多的职位,顺便提高大家的身价,也算是个好事情吧。 前端篇 199 前端常用的通信技术 [作者简介]陈为平,携程市场部前端工程师,目前主要负责“携程运动”项目的大前端相关 工作。 前段时间在忙开发携程运动项目和相应的微信小程序,其中和后端通信犹为频繁。get、post 请求方法是很多前端童鞋使用最频繁的;websocket 在 11 年盛行后方便了客户端和服务器 之间传输,……and so on ,除了这些,还有很多我们不常使用的其他方式,但是在实际的业 务场景中却真实需要。 本文总结了目前前端使用到的数据交换方式,阐述了业务场景中如何选择适合的方式进行数 据交换( form ,xhr, fetch, SSE, webstock, postmessage, web workers 等),并列举了一些示 例代码, 可能存在不足的地方,欢迎大家指正。 本文用到的源代码都放在 Github 上,点击下方阅读原文可直达。 关于 HTTP 协义基础可以参考阮一峰老师的《HTTP 协议入门》一文。 一、前端经常使用的 HTTP 协议相关(1.0 / 1.1) method  GET ( 对应 restful api 查询资源, 用于客户端从服务端取数据 )  POST(对应 restful api 中的增加资源, 用于客户端传数据到服务端)  PUT (对应 restful api 中的更新资源)  DELETE ( 对应 restful api 中的删除资源 )  HEAD ( 可以用于 http 请求的时间什么,或者判断是否存在判断文件大小等)  OPTIONS (在前端中常用于 cors 跨域验证)  TRACE * (我这边没有用到过,欢迎补充)  CONNECT * (我这边没有用到过,欢迎补充) enctype  application/x-www-form-urlencoded (默认,正常的提交方式)  multipart/form-data(有上传文件时常用这种)  application/json (ajax 常用这种格式)  text/xml  text/plain enctype 示例说明( form , ajax, fetch 三种示例 ) 前端篇 200 enctype

enctype 测试

表单提交: application/x-www-form-urlencoded

用户:

密码:

multipart/form-data

用户:

密码:

文件:

application/json

用户:

密码:

前端篇 205

text/plain

用户:

密码:

text/xml

用户:

密码:

服务端 form_action.php '; if($_POST){ echo "

POST

"; print_r($_POST); echo "
"; } if(file_get_contents("php://input")){ echo "

php://input

"; print_r(file_get_contents("php://input")); echo "
"; } 前端篇 206 if($_FILES){ echo "

file

"; print_r($_FILES); echo "
"; } * fetch api 是基于 Promise 设计 * fetch 的一些例子 mdn/fetch-examples 二、服务器到客户端的推送 - Server-sent Events 这个是 html5 的一个新特性,主要用于服务器推送消息到客户端, 可以用于监控,通知,更 新库存之类的应用场景, 在携程运动项目中我们主要应用于线上被预订后通知下发通知到 场馆的操作界面上的即时改变状态。 图片来源于网络,侵删 优点:基于 http 协义无需特别的改造,调试方便,可以 CORS 跨域。 server-send events 是服务端往客户端单向推送的,如果客户端需要上传消息可以使用 WebSocket 前端篇 207 客户端代码 var source = new EventSource('http://localhost:7000/server');source.onmessage = function(e) { console.log('e', JSON.parse( e.data)); document.getElementById('box').innerHTML += "SSE notification: " + e.data + '
'; }; 服务端代码 1, 'name'=>'中文', 'time'=>$time ); echo "data: ".json_encode($data)."\n\n"; flush(); ?> echo "event: ping\n"; // 增加 event 可以多送多个事件 js 使用 source.addEventListener('ping', function(){}, false); 来处理对应的事件 对于低版本的浏览器可以使用 eventsource polyfill 前端篇 208  Yaffle/EventSource by yaffle  https://github.com/remy/polyfills/blob/master/EventSource.js by Remy Sharp  rwaldron/jquery.eventsource by Rick Waldron  amvtek/EventSource by AmvTek 三、客户端与服务器双向通信 WebSocket 特点 1. websocket 是个双向的通信。 2. 常用于应用于一些都需要双方交互的,实时性比较强的地方(如聊天,在线客服) 3. 数据传输量小 4. websocket 是个 持久化的连接 原理图 图片来源于网络. 侵删 这个的服务端是基于 nodejs 实现的(不要问为什么不是 php,因为 nodejs 简单些!) server.js 前端篇 209 var WebSocketServer = require('ws').Server; var wss = new WebSocketServer({port: 2000}); wss.on('connection', function(ws) { ws.send('服务端发来一条消息'); ws.on('message', function(message) { //转发一下客户端发过来的消息 console.log('收到客户端来的消息: %s', message); ws.send('服务端收到来自客户端的消息:' + message); }); ws.on('close', function(event) { console.log('客户端请求关闭',event); }); }); client.html WebSocket 双向通信
说完了客户端与服客端之间的通信,现在我们来聊聊客户端之间的通信。 四、客户端与客户端页面之间的通信 postMessage 主要特点 1. window.postMessage() 方法可以安全地实现跨域通信 2.主要用于两个页面之间的消息传送 3.可以使用 iframe 与 window.open 打开的页面进行通信. 特别的应用场景 我们的页面引用了其他的人页面,但我们不知道他们的页面高度,这时可以通过 window.postMessages 从 iframe 里面的页面来传到 当前页面. 语法 otherWindow.postMessage(message, targetOrigin, [transfer]); 示例代码 postmessage.html (入口) 前端篇 211 postmessage 示例
左边页面
右边页面
post1.html Document
左边的 iframe
post2.html Document
右边的 iframe
五、Web Workers 进程通信(html5 中的 js 的后台进程) javascript 设计上是一个单线,也就是说在执行 js 过程中只能执行一个任务, 其他的任务都 在队列中等待运行。 如果我们执行大量计算的任务时,就会阻止浏览器执行 js,导致浏览器假死。 html5 的 web Workers 子进程 就是为了解决这种问题而设计的。把大量计算的任务当作类 似 ajax 异步方式进入子进程计算,计算完了再通过 postmessage 通知主进程计算结果。 图片来源于网络,侵删 主线程代码(index.html) 前端篇 215 Document
前端篇 216 后台进程代码( compute.js ) var i=0; function timeX(){ i++; postMessage(i); if(i>9){ postMessage('no 我不想动了'); close(); //中止线程 } setTimeout(function(){ timeX(); },1000); } timeX(); //收到主线程的消息 onmessage = function (oEvent) { postMessage(oEvent.data); }; 上述代码简单的说明一下, 主进程与后台进程之间的互相通信。 (携程技术中心市场营销研发部武艺嫱,对本文亦有贡献) 前端篇 217 长连接/websocket/SSE 等主流服务器推送技术比较 [作者简介]本文由携程市场营销研发部武艺嫱和王宇星以及张子祥共同撰写,武艺嫱在市场 营销研发部负责前端,王宇星和张子祥在市场营销研发部负责 java 后端。 最近做的某个项目有个需求,需要实时提醒 client 端有线上订单消息。所以保持客户端和服 务器端的信息同步是关键要素,对此我们了解了可实现的方式。本文将介绍 web 常用的几 种方式,希望给需要服务器端推送消息的同学在选型上有一点启发。 一、推送技术常用的集中实现的实现方式 1.1 短连接轮询: 前端用定时器,每间隔一段时间发送请求来获取数据是否更新,这种方式可兼容 ie 和支持 高级浏览器。通常采取 setInterval 或者 setTimeout 实现。 轮询示意图 通过递归的方法,在获取到数据后每隔一定时间再次发送请求,这样虽然无法保证两次请求 间隔为指定时间,但是获取的数据顺序得到保证。 缺点: 1、页面会出现‘假死’ setTimeout 在等到每次 EventLoop 时,都要判断是否到指定时间,直到时间到再执行函数, 一旦遇到页面有大量任务或者返回时间特别耗时,页面就会出现‘假死’,无法响应用户行 前端篇 218 为。 2、无谓的网络传输 当客户端按固定频率向服务器发起请求,数据可能并没有更新,浪费服务器资源。 1.2 长轮询: 客户端像传统轮询一样从服务端请求数据,服务端会阻塞请求不会立刻返回,直到有数据或 超时才返回给客户端,然后关闭连接,客户端处理完响应信息后再向服务器发送新的请求。 轮询示意图 长轮询解决了频繁的网络请求浪费服务器资源可以及时返回给浏览器。 缺点: 1、保持连接会消耗资源。 2、服务器没有返回有效数据,程序超时。 1.3 iframe 流: iframe 流方式是在页面中插入一个隐藏的 iframe,利用其 src 属性在服务器和客户端之间创 建一条长连接,服务器向 iframe 传输数据(通常是 HTML,内有负责插入信息的 javascript), 来实时更新页面。 前端实现步骤: 前端篇 219 1、Iframe 设置为不显示。 2、src 设为请求的数据地址。 3、定义个父级函数用户让 iframe 子页面调用传数据给父页面。 4、定义 onload 事件,服务器 timeout 后再次重新加载 iframe。 后端输出内容: 当有新消息时服务端会向 iframe 中输入一段 js 代码.:println("”);用于调用父级函数传数据。 优点: iframe 流方式的优点是浏览器兼容好,Google 公司在一些产品中使用了 iframe 流,如 Google Talk。 缺点: 1、IE、Mozilla Firefox 会显示加载没有完成,图标会不停旋转。 2、服务器维护一个长连接会增加开销。 1.4 WebSocket: WebSocket 是一种全新的协议,随着 HTML5 草案的不断完善,越来越多的现代浏览器开始 全面支持 WebSocket 技术了,它将 TCP 的 Socket(套接字)应用在了 webpage 上,从而使 通信双方建立起一个保持在活动状态连接通道。 原理: WebSocket 协议是借用 HTTP 协议的 101 switchprotocol(服务器根据客户端的指定,将协议 转换成为 Upgrade 首部所列的协议)来达到协议转换的,从 HTTP 协议切换成 WebSocket 通 信协议。 具体连接方式: 通过在请求头中增加 upgrade:websocket 及通信密钥(Sec-WebSocket-Key),使双方握 手成功,建立全双工通信。 前端篇 220 WebSocket 客户端连接报文 WebSocket 服务端响应报文 通信过程: websocket 是纯事件驱动的,一旦 WebSocket 连接建立后,通过监听事件可以处理到来的 数据和改变的连接状态。数据都以帧序列的形式传输。服务端发送数据后,消息和事件会异 步到达。WebSocket 编程遵循一个异步编程模型,只需要对 WebSocket 对象增加回调函数 就可以监听事件。 websocket 示意图 前端: 前端篇 221 服务端: 前端篇 222 1.5 Server-sent Events(sse): sse 与长轮询机制类似,区别是每个连接不只发送一个消息。客户端发送一个请求,服务端 保持这个连接直到有新消息发送回客户端,仍然保持着连接,这样连接就可以消息的再次发 送,由服务器单向发送给客户端。 原理: SSE 本质是发送的不是一次性的数据包,而是一个数据流。可以使用 HTTP 301 和 307 重 定向与正常的 HTTP 请求一样。服务端连续不断的发送,客户端不会关闭连接,如果连接断 开,浏览器会尝试重新连接。如果连接被关闭,客户端可以被告知使用 HTTP 204 无内容响 应代码停止重新连接。 sse 只适用于高级浏览器,ie 不支持。因为 ie 上的 XMLHttpRequest 对象不支持获取部分的 响应内容,只有在响应完成之后才能获取其内容。 前端篇 223 二、常用实现的对比 三、项目选型 Websocket 需要服务器重新部署,sse 可以利用原先的 http 协议,而我们项目是在高级浏览 器环境,场景是需要服务器单向发送给客户端,所以 sse 更符合我们的需求。 四、项目实践 A 应用下单完成后,把订单消息放入到 redis 缓存中,B 应用去获取 redis 缓存信息判断是否 是新订单,否的情况轮询 redis 缓存,是的情况消息推送给前端。 前端篇 224 后端流程图 客户端: 然后使用 onmessage 事件来获取消息 服务端可以自定义类型的事件,对于这些事件,可以使用 addEventListener 来获取。 服务端: 内容与普通的 Controller 差不多。只不过相应的方法在路由配置时,将 produces 属性的文 本类型设置成“text/event-stream”即可。 前端篇 225 首先通过设置唯一标识符+venueid,获取相应场馆保存在 redis 中的订单。 常见问题及解决方案: 1、怎么确定推过来的消息是新消息 这里我们设置了一个本地缓存,用来存放上一次从 redis 中获取的信息,和当前从 redis 获 取的信息做对比,不同,则认为是新信息返回给客户端并标识是新数据。 2、刷新页面原先推送过来的消息消失了 因为在通过 redis 和本地缓存对比的时候没有区别所以不会推送,这里前端设置一个随机数 num,在存入本地缓存时 key 值多加了 num 的区分。 3、解决容器超时的问题 后端容器的单个连接超时时间为 2 分钟,后端每隔 3 秒钟会轮询一次 redis,到第 20 次的时 候,会推送个带有个标识的数据。 4、接口防刷方案 后端记录每次获取到的 num 值判断总数 vnum,超过一定数量返回 http 204 断开连接。 前端篇 226 总结 对于简单的推送需求又不考虑兼容低版本浏览器,推荐使用 server-sent Events。 如果需要多条双向数据实时交互或需要二进制传输,推荐 websocket。 对于还要考虑低版本浏览器,那么还是用轮询来实现功能。 前端篇 227 如何一步步打造基于 React 的移动端 SPA 框架 [作者简介]喻珍祥,携程港澳研发高级经理,2004 年接触互联网开发,见证前端开发从美工 到全栈开发的全过程。2014 年加入携程,主要负责永安旅游 APP 移动前端架构和研发。 现今前端新技术井喷一样层出不穷,且各有特点和使用场景,交互变得前所未有的复杂,那 么,在众多框架中,如何选择又如何落地呢? 前端框架作为工具,是各种模式,结构的集合,一个原则就是:“如非必要,不换”。但是, 打算换一定要有换的道理,首要的原则就是当前的框架已不适应业务的发展,而框架就是要 解决业务扩展性的问题。技术选型应从实际出发,透过各种框架的本质,了解它们使用的场 景,选择接近自身业务和利于团队成员现状的框架,接着使之工程化,自动化,让它与实际 严丝合缝,成为协助业务发展的利器,这不能不说是件非常有挑战力的事。 本文以实际项目为例,给大家分享一个前端业务框架设计和实践过程,其中有对框架设计的 考虑,对某些技术点应用场景的处理,以及踩过的“坑”。 一、构建前端业务框架前的思考 程序员在设计业务框架时很容易陷入技术思维的陷阱:用最新最牛的技术,要做大做全。如 果只是执着于这两点,就会忽略成本,适用范围,使用者的满意度等这些重要因素。 理解开发框架的价值 一个业务框架的价值就是让基于它开发出的产品质量高,开发过程高效,开发成本低,还能 给开发人员带来幸福感。 前端篇 228 明确开发框架的目标 开发目的我们分为业务目标,技术目标和人事目标三部分。 定义框架适用的场景或范围 前端篇 229 当我们理解了框架的价值,明确了开发的目的,我们就需要定义适应场景和范围,我们需要 对产品目标用户的环境进行确定,这对开发,测试以及产品三个层面都是有意义的。产品预 测和定位用户群体,开发用来预算开发周期和成本,测试用来确定用例的边界和测试的范围。 二、技术选型与实用性分析 框架开发技术选项首先要考虑的是提供什么功能才能让业务系统开发人员更加方便开发。 SPA 作为用户体验好的一种产品类型,用户体验是框架开发需要考虑的一个重要因素。我们 团队开发的产品属于典型的电商产品,业务框架需求也跟大多数电商公司相同。下面就是我 们选定的基础层次结构和实用选型思路。 前端篇 230 开发规范 无论是从代码清洁度、可维护性、健壮性、还是团队配合效率,我们在开发框架时的第一步 都是制定和确定团队的开发规范。我们前后端统一用 CommonJS 模块化、基于 React 组件 化、用部分 ES6 特性、CSS 用 LESS 编写,最后我们定义了这些:  前端编码规范(基本的 JS,HTML,CSS 最好写法)  项目/目录结构规范  组件化编写和拆解规范  React 编码规范  MVC 结构的定义,Controller,Actions,Reducers,React 组件之间职能的划分  ES6 使用指南及限制。例如:class、import,对象表达式等是禁止使用的。依据是使用 这些特性后,Bable 生成后的代码特别多,导致文件增大,影响加载性能。  LESS 编写规范  第三方库引用规范 Infrastructure 包含所有的基础建设模块和应用启动生命周期,这里介绍几个常用的模块。 Infrastructure - 存储器 基于浏览器 sessionStorage、localStorage、memory 和 cookie,框架提供 SessionStorage、 LocalStorage、MemoryStorage 和 CookieStorage 四种存储器。目的是更好地控制缓存,下 面是存储器的主要实现: 前端篇 231  统一四种存储的方法调用,规范了增删改查接口,方便插拔式调用。  支持 JSON 数据直接读存  支持过期时间设置,和过期数据自动清理  支持浏览器存储超出限额后,自动清除过期时间最早的数据  支持版本迭代后,数据自动清除 Infrastructure -用户标识 ClientID 机制,用户唯一标示,用户初次启动应用时为每个客户端 LocalStorage 中存储 ClientID,用于分析用户行为,对于错误处理和行为分析非常有帮助。 Infrastructure - 错误处理 框架集成错误处理,通过 onerror 事件,将客户端错误信息直接回发到用户行为系统。错误 信息包含用户唯一标示 ClientID、错误发生的文件以及行数,这样能使我们能及时掌握生产 上的错误,并能快速定位问题。 Infrastructure – 继承 实现组合继承和对象扩展机制,支持构造函数和多对象扩展。不用 ES6 继承的原因是避免 Webpack 解析出的代码太多和冗余,导致文件增大。 Router 路由是 SPA 必不可少的一个模块,我们没有选择 React-Router,而是自己去开发。其原因 有三:  H5、Hybrid 以及服务端端实现路由规则同构。我们 Hybrid 是属于静态文件直出,只支 持 hash,而 H5 需要使用 HistoryAPI 来和服务端路由同构。  我们业务系统相对比较复杂,部分系统超过 30 页面,需更灵活的路由规则来配合 APP 运行生命周期,比如异步加载、页面缓存都是根据路由来做的。  我们原有框架已经实现基于 hash 的路由,团队成员也非常熟悉,所以实现和应用成本 都非常低。没必要去趟 React-Router 这趟浑水了。 我们路由模块实现思路:H5 端基于浏览器 popstate 事件,Hybrid 端基于浏览器 hashchang 事件。同一套路由在启动时根据判断环境自动切换,与服务端实现对相同的路由解析规则保 证这部分代码同构。 MVC MVC 最开始考虑用 Backbone,但发现结合 React 后存在的意义不大,还需要在它的基础上 扩展上我们的应用生命周期,成本跟自己研发一样,果断放弃。我们结合 Redux 形成了现在 的 MVC 模型。 前端篇 232 MVC – Model Model 职责相当于操作业务数据的 ORM,属于三层中代码最重的一块,基本业务逻辑都在 这层实现。在我们业务框架实现 Model 基类的时候,我们考虑业务系统开发时仅需要根据 业务场景做这些实现和配置。  配置 Ajax 调用参数,例如路径、Method、是否缓存等。  可以实现 Ajax 调用参数的格式化方法以及结果格式化方法。  可以配置存储器缓存参数和结果。  如有需要子类可以重写基类的 execute 方法,改变 Ajax 调用方式。基类 execute 默认执 行 Ajax 请求并返回 Promise。  实现 Model 时也可以不配置 Ajax,仅当 Model 为一个本地数据存储实体。 MVC – View View 的职责还是负责页面展示,这层我们选用了 React,原因如下。  页面在复杂交互中渲染更快,同时用它来实现组件化。  相比 Vue,我们团队成员更熟悉。  相比 Vue,我们将来迈进 React Native 将更近。  JSX 比在模板中嵌入表达式更适合 JavaScript。 我们没有将整个应用作为一个大组件,而是为每个页面创建了一个容器,在每个容器中插入 页面组件,页面组件中调用其他 UI 组件。这样做的目的为了让数据分到页面,数据量分散, 解析和操作时性能更好。 MVC – Controller Controller 的职责是负责将 View 跟 Model 串联起来,同时提供 Redux 的功能。如上图所示, Controller 中的 States Manager 就是 Redux 中的 Reducers 和 Store。 前端篇 233 引入 Redux,目的是为了解决 React 自身状态管理太乱。但我们还是进行了两点改造:一是 用基础类库中的函数替换它使用的原生方法,减少代码量;二是扩展存储方式,使他支持我 们的存储器。 StatesManager 中的 Store 主要存储页面上状态数据,就是我们挂载的存储器。分为页面存 储器和应用存储器两种,其中页面存储器存储当前页面的状态,而应用存储器全局状态和全 局数据。 Vendor 第三方库,包含 React-Lite,React,ReactDOM,FastClick,Underscore,Zepto 等,方便开 发时使用和后期定期更新。 Services 包含了广告操作,定位操作,地图操作,旅客操作,登入登出,优惠操作,Native 操作等公 共的服务。存在的目的是调用方不用关注数据源头和去向,只需关注功能本身即可。 Plugins 包含了 Underscore 的扩展插件,Webpack 错误处理插件,统计收集插件,平台/浏览器兼容 插件等。存在的目的是为了封装一些需要在应用/页面生命周期中执行,但不能破坏生命周 期的一些公共模块。 UI Components 这层主要包含公共组件,起码需要提供常用纯组件和常用的业务组件。我们这里提供了各种 表单组件,列表展示组件,预加载组件,日历组件,广告组件等。 Hybrid Shell/Bridge 这层属于独有的,根据 Native 提供的方法写的。Native 通过 JS Core 提供了一系列的全局 JS 方法,而 Hybrid Bridge 就是将这些方法分类型封装起来,作为与 Native 通讯的桥梁。 HybridShell 实现一套事件订阅机制来实现 Hybrid 代码和 Hybrid Bridge 的通讯保护机制,保 证无论 Bridge 中是否存在相应的方法,或者调用参数是否错误都不影响 APP 的运行。 Server 服务器选用 Nginx+Node+PM2,这样的搭配无懈可击,稳定,高性能,高可用。Nginx 和 Node 都是可以做单台/多台集群的,充分利用资源,对于 H5 站点轻松应付。PM2 主要为了 守护 Node 进程,但为了保证它的稳定性,我们加了双保险,除了监控,还加了系统级别的 守护进程。 前端篇 234 三、构建脚本执行生命周期和开发流程 脚本执行生命周期,即是将脚本执行过程拆解成一系列的顺序阶段。目的是为了对整个应用 做更好的控制,让复杂繁多的代码更清晰。同时也便于开发人员理解整个脚本执行过程,对 后期性能优化也非常有帮助。我们的框架分为框架应用脚本生命周期和页面脚本生命周期。 框架应用生命周期 框架开发人员在开发过程中定义好每个阶段职责。当我们定义的阶段职责明晰后,后期性能 优化就有了一个非常清晰的路线图。从性能角度上看,在“进入页面生命周期”这个阶段前, 都会是白屏时间,我们在每个阶段都加入了性能埋点数据,可以清楚的知道每个阶段的耗时, 后期可以根据这个耗时来进行优化了。 页面生命周期 框架开发人员负责定义好这个流程,业务开发人员负责用业务代码来填充这个流程。和应用 生命周期一样,对性能优化也有重大意义,同时给业务开发人员编写也提供了一个根据页面 生命周期编写的开发流程。 前端篇 235 如下面代码,一个页面控制器的写法 四、前端插件化/组件化/服务化/模块化的应用 前端篇 236 组件化、插件化、服务化和模块化并非为前端而提出,在后端存在已久,都非常成熟。而他 们的目的就是“高内聚”和“低耦合”。 组件化 引入组件化能使我们在开发和维护中节省了大量的工作。因为新业务框架上线后,我们需要 超过 8 个系统几百个页面要改版,无论从 KPI 还是个人幸福感都需要在开发业务框架时引入 组件化。 我们引入组件化,可以获得以下好处:  开发方便,无需关注组件的依赖关系  测试方便,写完组件即可测试。  职责清晰,独立开发,功能高内聚  接口规范,维护性更高  框架层直接提供大量公共组件,缩短业务系统开发时间  灵活组合,方便调用,效率更高 选择组件化标准 从长远来看 Web Components 是一个很好的选择,是未来的一个组件化的标准,受到 Google 的大力推进,Chrome 已经全面支持,其他浏览器也是紧随其后开始支持和兼容,到写这篇 文章时已经基本支持了。但我们当时为了更好的兼容性和服务端渲染选择了 React Components。 ReactComponents 相比 Web Components 没那么规范,样式隔离性也不好,但它的组件状 态管理机制和渲染算法还是非常具有竞争力的。我们在 React Components 的基础上将所有 UI 都是进行组件化,现阶段组件化的做法:  将职责单一,能独立开发,测试和维护的 UI 块划分为组件。  分为容器组件和功能组件,容器组件用来组合功能组件。  组件封装出 CSS 外的所有功能。常用公共组件 CSS 跟框架样式文件一起打包,而非常 用公共组件 CSS 则需要单独在项目中引入。这是我们做的不是非常好的地方,这样做 的目的是为了减少 CSS 引入大小和利用 CSS 文件缓存。  尽量将组件定义为无状态组件,增加复用度。 插件化 可以这么概括插件化,在应用开发完成后,希望不修改原有应用情况下,将新功能插入到应 用系统中,这就是所谓的插件化。插件化的最大优点就是不破坏原有程序和生命周期,现在 流行的框架中用到极致的就是 Webpack 的插件系统。 我们按下面三种规则来定义插件: 前端篇 237  需要插入到应用或页面生命周期的某一个环节的功能  该功能可以独立封装,不依赖外部功能  多系统或页面共用。 例如:扩展 Underscore 的一些方法插件,收集统计插件等 服务化 服务化在前端很少被提到,多用于服务端 API。可以这么概括服务化,将一些特定功能由提 供方以服务的形式提供出来,应用方不用关注其实现方式,只需关注调用功能即可。 服务化在后端很好理解,前端如何理解?每个特定功能都能看成一项服务,可以是组件,插 件,以及单独的功能模块;把这些功能都封装部署在一个特定的站点,业务系统需要用的时 候直接异步调用这些服务的地址即可,不用关注其依赖和实现过程。 在我们的 SPA 框架中,把一些功能组件和模块当成服务,业务系统不需要预先引进,只需 要在用的时候调用相应地址就可以了。目前做的不是非常好,主要是这些功能模块和组件, 还依赖业务系统引用我们的 SPA 框架,如果要做到极致,就不再需要关注这些了,我觉得 这将是前端未来几年一个趋势。 例如:不常用的公共组件,不常用的公共功能模块 模块化 模块化自从 CommonJS 出来后就成为前端的架构热点。模块化就是功能拆解,将小功能内 聚,拆解系统耦合。也就是说拥抱模块化就能避免在代码中嵌入依赖关系。这里不做过多讨 论,网上资料很多,只讨论下面几个问题。 是否需要模块化?模块化毋庸置疑,不做模块化前端就无法完成复杂的系统开发。只要你编 程技能在提升,你就会不知不觉对代码功能进行模块化,跟你使用什么类库没关系。不是你 不使用 CommonJS,AMD,CMD,ES6 就不能模块化,一个对象都可以算一个模块。只是 CommonJS 这些类库规范了模块的定义,使用和依赖关系的调用而已。 模块化还是组件化?模块化和组件化并非矛盾关系,而是一种包含关系。像上面写到的,组 件、插件和服务都属于模块的一个子集。对于我们做 SPA 时定义的就是组件跟 UI 有关,非 UI 相关的模块细化为插件和服务,以及不能区分开的功能模块。 ES6 还是 CommonJS?很多同学讨论 ES6 可以实现模块化,用 Node 写后端还用 CommonJS 吗?其实这是不是重点,重点是你的项目成本和成员喜好,并不妨碍你写一份优雅的代码和 实现一个伟大的产品。我们就是前后端都使用了 CommonJS 的模块化写法,前端利用 Webpack 打包时来做解析。 五、Hybrid 性能痛点及处理方案 前端篇 238 Hybrid 作为现在流行的 APP 开发模式,拥有着跨平台、迭代快、开发体验好等明显优势, 同时也存在着加载慢,用户体验差这两个痛点。如果要像 Native 一样的体验,H5 真的很难 处理,H5 无法控制,我们需要 React Native。那这里只讨论“加载慢”这个痛点。 我们把 Hybrid 的“加载慢”问题拆分为下面 3 个点。 1. WebView 打开慢 根据我们的测试无论Android还是iOS首次初始化WebView时所花费差不多要300~400ms, 第二次初始化需要 100~200ms 左右。可以看出第二次初始化要快一些,所以这里我们可以 通过提前初始化一个 WebView 来提升性能,或共用一个 Webview。 2. 页面加载慢 如果页面在服务器端渲染这个问题会比较大。我们选择静态直出,将 Webapp 包嵌入到 APP 中,基本页面可以达到秒开。 静态直出带来一个问题是如何实时更新?我们 Native 端做了一套更新机制,可以根据 API 的数据实行打开 APP 就更新静态文件。我们只要保证打包 Webapp 将 Webpack 打包的模块 ID 固定不变,这样我们就可以在提交更新包时做文件差异化比较,更新包会非常下,加载也 会很快。 3. 页面脚本资源加载和解析慢,数据资源加载慢 这一环节是性能优化的重点,优化不好直接导致了白屏时间过长。因为静态直入方式,页面 基本在 300ms 内会出来,所以我们做下面几个优化操作。 第一步,我们将页面调用的种子 JS 文件精简到最小,然后页面加载完后再去异步加载和执 行其他 JS 文件。这样做的目的是使 Android WebView 尽早触发 OnPageFinished 事件,减 少白屏时间. 第二步,接口缓存数据,接口缓存数据即是每次请求接口的数据根据业务场景设置缓存时间, 在这段时间直接使用不再调用接口,这样只有渲染消耗了。 第三步,有了接口数据缓存,但仍没有解决首屏数据首次记载的问题。这一步就是通过在发 布 APP 前,打包最新首屏接口数据以 JSON 的格式一起打包到 APP 中,同时首屏图片资源 也一起打包进 APP。在页面展示时先从本地取数据展示,然后再请求接口,等到接口返回最 新数据后替换掉页面数据和本地缓存中的数据,保持数据新鲜度。 第四步,有了前三步还是有部分白屏时间,特别是首屏组件复杂的情况下。我们紧接第三步, 打包时我们不再只将接口数据打包成 JSON 文件,而是直接生成 HTML 到首屏静态文件,只 要页面打开就能看到内容了。这也是我们最近正在优化的一步。 前端篇 239 第五步,有了第四步,白屏时间已经缩短许多了,但会发现出来了页面却不能操作的情况, 这就是这步需要去做的,通过减小初始化执行代码量和减少和 APP Native 代码的交互来解 决脚本解析慢的问题。这是我们将来的一个优化方向。 这其中第 3 点是所花的时间最多,效果最不明显,可以考虑在后期再慢慢优化。 六、同构:基于 Node 的 SPA SEO 解决方案 “Write once,run everywhere”这是一句形容 Java 的语句。现在 Node 出来后 JavaScript 也可以用这句话来描述。一份代码同时在客户端浏览器和服务端 Node 运行,这就是 JavaScript 同构。 SPA 的硬伤是首屏性能差和几乎达不到 SEO 效果,这导致很多需要 SEO 和首屏快速渲染的 应用不会使用 SPA 这种模式。而小部分 SPA 应用通常用下面两种方法来处理这块硬伤。 1. 用服务端语言重写一套页面给搜索引擎用。 2. 理解 JavaScript 解析器在服务端来解析客户端的脚本语言,例如服务端嵌入 V8 解析器。 前者属于高成本的方案,而后种属于低性能方案。所以我们基于 Node,利用 JavaScript 同 构来解决 SPA 的这两个问题。 理想的前后端同构方案 目标:前后端同构数据 Model、页面 View,路由规则以及一些工具类方法。 同构 Model 层代码 Model 作为连接前端展示和后端业务数据的重要层,前面有讲到,它包含了接口名称,接口 调用方法,数据格式化方法和缓存处理,以及一些错误处理方法。而接口调用方法和缓存处 理这两块客户端和服务端的实现有所不同。要同构,客户端与服务端的调用方式必须相同, 而我们需要 Node 做到以下三点即可:  写一个类似 Ajax 的方法,将接口调用方法由原来的 XMLHttpRequest 替换成 Http 模块 请求。保证调用使用相同方法。  写一个存储器基类,处理原来 Model 的本地缓存机制,这里可以用 Redis 或 Node 变量 实现。  重写一个 Model 基类,方法属性跟前端框架中的 Model 基类一样。  为避免模块调用依赖,客户端和服务端的 Model 必须都是两个,一个是无依赖模块的 纯数据处理 Model,另一个 Model 基于这个纯数据处理 Model 的扩展,区别于服务端 和客户端,分别加载不同的依赖。 同构 View 层代码 我们框架没有实现这块同构,原因: 前端篇 240  我们 SPA 中的 React 组件相对复杂,依赖模块也较多,我们必须跟 Model 一样抽离出 一个纯展示组件。而对于一个所有操作都由数据流控制的 React 组件,要抽离一个纯展 示组件来兼容成本高。  SEO 和首屏渲染需要的 React 组件非常简单,而且必须简单,这样才能提高首屏渲染效 率,就算不复用成本也不高,也没有必要和 SPA 的结构时时保持一致。 我们的这层处理方案:服务端和客户端用了两个不同 React 组件来处理,服务端组件仅包含 首屏的数据结构,在服务端通过 Node 渲染好,呈现给用户和搜索引擎。这样搜索引擎能搜 到内容,用户打开网页也可以跳过 JavaScript 加载和渲染这段白屏时间。 同构路由规则和工具类层代码 路由规则重构非常简单,在 SPA 框架的路由规则支持 Express 路由即可,然后路由规则放一 个模块中前后端同时调用即可。 工具类更不用说了,都是 JavaScript,语言上就可以重用。只是要注意,这些工具类都是不 依赖其他模块的。 最终的方案  客户端服务端的脚本写法我们都遵照 Node 的 CommonJS 规则。  同构 Model,路由规则和工具类  服务端负责处理路由规则,调用自身的页面展示组件,生成 HTML 再呈现给用户。HTML 中还包含本页所需数据 JSON 数据(由于这些数据服务端已经请求好,避免客户端再掉 接口获取,作为初始化数据返回)。  客户端 JavaScript 加载完后,判断 HTML 中有初始化数据,用这些数据重新渲染当前页, 并绑定各个事件。 最后一点大家可能疑问,为什么这样?这样会出现渲染两次的。没错就是渲染了两次,这就 是我们现在框架需要改进的方向,我们将来的方案应该是利用后端提供的数据绑定页面上的 React 组件,而非重新渲染。 七、SPA 和 React 结合的思考 SPA 的优势是体验好,但由于脚本操作 DOM 渲染,在复杂的富客户端情况下,导致渲染速 度是最大的性能瓶颈。而 React 就是为解决富客户端渲染速度问题而生的一个框架。框架总 是在解决问题的同时会带来新的问题,我们现在就来看看我们碰到的新问题。 性能没有得到解决 本打算用 React 来解决性能问题,但用后才发现性能问题仍没得到解决,甚至比原来还差。 我们总结了几点: 前端篇 241  React 文件太大,导致加载 JS 耗时增加,导致首屏慢。此问题可以用 react-lite 代替 React 上线来解决。现在随着 React 的升级,该问题也会被官方慢慢在解决。  首页组件数据太多,数据同时渲染,导致渲染耗时增加。解决此问题需要拆分组件,数 据分部渲染。对于需要请求数据的组件可以用默认数据填充或加载中组件临时替换。  组件绑定数据太大,导致每更改一个属性导致整个组件刷新。解决此问题需要做两点, 首先要思考该组件是否需要绑定这么多数据,其次可以用 shouldComponentUpdate 来 优化。 数据流控制与 Redux React 的状态机制很强大,所有 UI 变化都有状态来控制。但如果状态太多,特别是对于组件 间经常通讯频繁的情况,靠自身的状态管理机制来处理太复杂了。为了解决这个问题,我们 引入了 Redux 来管理 React 的状态机制。事物总是辩证的,Redux 的引用也一样,带来好处 的同时,也给我们带来了烦恼,我们总结了一下。 思维大转变与全局公共组件调用 当业务开发人员写业务代码时,以前关闭和打开隐藏一个加载组件,只需要写一行代码即可。 但现在我们告诉他,你不能这么做,你需要通过 dispatch 一个 action,然后在 reducer 识别 到这个 action,并将 store 内的属性更改,然后 reducer 返回一个新的 state。大家都觉得相 当复杂,包括我自己都这么认为。 这其实是在项目前期,我们心里对 Redux 还是有所抵触,思维要从原来的操作 DOM 到操作 React 这种状态操作,同时对从 React 直接操作状态到通过 action 去更改组件状态,的确需 要时间消化。于是我们还是把这些基础方法定义在了我们的全局对象上,同时在基类实现了 这些复杂的操作,业务只需要调用这些方法发送相应的 action 即可,还按原来的方式调用。 我们是否真的需要 Redux? 当我们用到 Redux-devTools 这个插件后,充分看出 Redux 可预测性好处。但用了一年多后 还是做了这个思考:我们是否真的需要 Redux?原因是 Redux 有很多束缚,很多简单的页 面,严重增加了代码的复杂度和开发时长。Redux 优势是管理复杂的状态,而我们大部分场 景的复杂度可以通过一些内部状态和高阶组件的方式来规避,而不一定要 Redux。 于是我们开始考虑,Redux 的思想非常好,我们需要保留。action 和 reducer 有些情况是否 真的有必要写? 这些问题都在等待我们解决,这里不深入,因为我们也只是思考中。提到 的目的是让大家在实现自己的移动业务框架考虑一下自己的应用场景是否真的需要 Redux。 八、我们如何实现工程化,自动化 最后我们来我们在做这个 SPA 框架时如何实现的工程化。 1. 技术选型时,我们就做了一系列的代码规范。框架开发完后有提供了一些说明文档 Native 通讯说明,数据存储说明,全局变量及工具类说明,模块按需加载说明,组件编写说明等。 前端篇 242 2. 模块化,组件化,插件化,服务化严格定义了模块的分工和应用规范,提供公共组件、插 件和服务模版参考。 3. 利用 JSDoc 生成框架各模块使用说明,开发一个 UI 通用组件展示站点,方便公共组件应 用和推广。 4. 实施敏捷开发,明确阶段目标和版本计划。使用敏捷,主要目的是让开发过程更透明,更 稳定和高效。线上敏捷系统方便各级项目管控;线下白板,小组内部实时沟通,方便跟进进 度和处理紧急任务。 5. 统一开发 IDE 为 VSC。利用 IDE 的插件和代码片段功能,自定义框架的代码提示和补全 片段和插件,降低开发成本。同时统一配置 ESLint,CSSLint 插件,随时检测代码质量。 6. 一键自动构建业务系统脚手架,参考 Grunt-init。 7. 实现开发代码和浏览器代码自动同步,利用 Webpack+ Browser-Sync 保存代码自动刷新 浏览器。 8. 代码自动化构建 Gulp+Webpack 实现代码的解析,压缩合并,异步加载,数据缓存等, 达到一键构建。 9. 自动化单元测试 Karma+ Jasmine 配合 Jenkins,Webpack,实现打包和构建前先运行单 元测试。 10. 持续集成部署,Jenkins 加各种插件实现持续集成,一键打 APP 和 H5 最终发布包,同时 非生成环境的自动部署和一键部署功能。 前端篇 243 11. 将用户访问的性能和错误数据实时反馈到服务端,定期分析和修正。 12. 代码 Review+持续学习+鼓励创新,提高团队自身实例。 自动化测试 单元测试,我的目标 TDD。TDD 对于前端开发人员的要求非常高,主要是思维模式上。这 是我们的一个方向,我们现在单元测试这块主要做了一些必要逻辑的单元测试,未做到全系 统。主要使用的框架:Karma + Jasmine。其中 Jasmine 负责测试代码部分,Karma 负责自动 化。 写单元测试要注意的几点:  不要写对接口返回结果测试的代码,那是接口测试的范畴。单元测试只关注传值是否正 确。  业务代码不要写对框架方法的单元测试,业务代码只需要验证调用的方法和传值是否正 确。框架的单元测试代码自有框架去写。  不要写能功能测试,单元测试是对单个方法逻辑的检验。如果要涉及到多个方法或这个 功能依赖,要么单元测试思路有问题,要么就是代码需要重构。  不要追求 100%覆盖,特别是时间仓促的情况下。  TDD 比后补单元测试更节约时间。 持续集成与自动化构建 我们整个持续集成如下图,我们持续集成分开发,构建,测试和部署四块。 持续集成整个过程中,出了开发写代码和人工测试这两个过程,其他过程基本都能自动化实 现。 自动化构建 前端篇 244 自动化构建,我们分两块:Webpack 构建和 Jenkins 构建。Webpack 主要 follow 在代码级 别,而 Jenkins 则在工程级别。 Webpack 打包,存在开发和构建两个阶段。构建阶段的“应用打包”即是开发阶段的整个 打包过程。主要用到了 Webpack,Gulp,Babel,Browser-Sync,ESLint,CSSLint,Karma 等。  ESLint,CSSLint 检查代码写法。  利用 Babel 解析代码 ES6->ES5,LESS->CSS,JSX->JS。  处理异步请求代码  运行单元测试并生成报告。这属于可选步骤,如果开发时可以关闭,单是 Jenkins 构建 必须走的一步。  压缩和优化代码。  开发模式下,更改代码后自动更新浏览器内容。  Hybrid 模式下,下载最新生产首屏内容数据打入包中,降低 APP 下第一次打开时的白 屏时间。  框架打包时,生成框架全局变量 VSC 代码提示片段。 Jenkins 构建,整个构建和部署阶段都可以在 Jenkins 上完成。目前我们除生产部署外,其他 环境都在 Jenkins 上进行。简单的说 Jenkins 构建就是将打包的各种人工操作集成到一个 Job, 实现自动透明的打包和部署操作,而整个过程生成完后,我们还能看到整个生成后的结果报 表。 Dev: 开发人员提交代码,Jenkins 就自动拉代码,做好打包准备,运行 Webpack 打包,打包 完后发布到 DEV 站点。打包到 DEV 站点的代码都是经过代码质量检测和单元测试的,明显 问题不会很多。 Test:测试人员准备测试时,在 Jenkins 点击 Test 环境构建 Job 就可以一键部署了,过程和 dev 相似,不同的是 Test 环境发布的同时这时候会生成一个以产品版本号、日期和构建版本 号命名的包到 GIT。如果这轮测试没问题,这个包将会成为生成发布包。 Prod:我们集群部署公司自己编写的软件发布,取 GIT 上通过测试的包。当然这个工作 Jenkins 也是可以胜任的。 前端篇 245 持续集成在整个工程化过程中也是非常重要的一环,而整个持续集成过程中自动化测试为不 可或缺的一部分。我们现在只做到了代码自动化测试,如果做到 UI 自动化测试这就更完美 了。UI 自动化测试也是我们将来的一个方向,通过 selenium 来实现已经在我们的日程中, 我相信 UI 自动化后,会使整个工程化的效率更高。 最后总结,在整个开发的历程中,我们知道没有最好架构,只有最适合的方案。 Facebook,Google 等公司的方案,我们可以参考,但不能照搬。每个团队都有自己的特色, 在开发业务框架的过程中,我们要多利用团队的优势,多思考自身团队整体能力区间以及产 品所处的应用阶段,多考虑成本和效益,从重点功能着手,以多次迭代的方式来开发和完善 它。最后分享一张我们框架基础架构图给大家参考。 安全篇 246 安全篇 安全篇 247 携程是如何保障业务安全的 [作者简介]王润辉,携程技术中心信息安全部高级经理。2015 年加入携程,负责携程业务安 全。个人专注在:安全漏洞,数据分析建模,业务安全,风控系统整体架构等。 作为国内第一大 OTA 企业,业务安全一直是携程所面临的重要安全风险之一。 在面对各类从散兵作战到越来越专业化的黑产,以及技术从单一到持续自动化的工具化下的 攻击时,我们也根据不同的业务安全风险,建立了相应的系统进行防护,并和黑产进行持续 的技术和思维上的攻防。 其中经历了从业务驱动技术(被动式防御),到技术驱动业务(主动式防御)的过程,不断 结合新技术的应用,新系统的开发以及注重用户体验和安全的平衡,并最终关注用户安全感 知,不断完善业务安全的各方面,为用户提供一个安全、可信的环境,同时减少企业在安全 上的损失。 一、携程业务面临的四大安全风险 携程的业务当前面临了大多数互联网企业都面临的相关业务安全风险: 1、垃圾注册,但发现有较多难点:手机号码;秒拨 ip;行为工具化;打码平台等; 2、扫号也是重大安全风险之一,威胁账号安全的关键点:资金盗用;信息泄漏;恶意欺诈。 同样发现也有很多难点如:IP 使用量巨大,可以做到 1 号 1IP;使用外部社工库,密码正确 率高;可以根据安全措施及时更换策略;设备指纹基本伪造,无明显特征; 3、薅羊毛(羊毛党)是当前电商,金融类公司重大的安全风险。对企业产生较大影响,包 括影响活动实际收益和到达率;侵占有限的活动资源。发现难点:牟利方式多样化,各种形 式组合;模拟真人或直接真人操作;黑色产业链发达,集团化模式; 4、爬虫,企业的价格策略被掌握;扰乱 PV/UV,无法做出正确营销判断;发现难点:频次 低,特征不明;不会对业务方造成明显感知; 二、业务驱动技术 面对上述四大业务风险,携程设计了三大系统进行应对。 1、统一验证码 各应用验证码难度解耦;自动调节难度和类别;各参数独立可变配置;服务响应在 10ms 之 内; 安全篇 248 系统概况: 应对场景:扫号,薅羊毛,接口遍历; 效果:数字英文易识别成本低,中文输入成本高; 体验:用户每天需要一定时间输入,英文数字在 5s 左右,中文大于 10s; 成功率:成功率优化后保持在 90%; 架构图: 后端验证码配置界面: 安全篇 249 问题:类别单一,易于破解(机器识别,人工打码);体验较差,输入验证码时间长;识别 率存在上限; 2、风控系统 实时配置规则;异步响应; A/B testing 系统概况: 应对场景:扫号,薅羊毛; 效果:日均响应请求 1000w+次; 性能:平均响应耗时 5ms; 系统架构图: 安全篇 250 风控系统配置界面: 问题:非实时响应;非多参数响应;无法支持多数据源;规则引擎服务写死 3、风险数据平台 基于离线规则运算;数据迭代,分钟级别计算结果;与外部黑产数据结合; 系统概况: 应对场景:垃圾注册,扫号,薅羊毛,爬虫; 对扫号近实时拦截,目前成功账号/IP 已经达到了 0.7:1; 反爬主要提供对恶意爬虫的检测,经过 A/B 测试日均拦截爬虫行为 10w 次; 安全篇 251 薅羊毛和异常注册识别月均识别超过 10w 次; 案例: 2016 年 7 月 27 日早晨 10 点 17 分,出现大量异常登录请求,风险数据平台在 10 点 25 分 发现异常,迅速介入,10 点 45 将异常登录请求降低到十位数,在 11 点降低到个位数,对 方此时发现无法绕过,直接放弃本次扫号。 本次事件持续 40 分钟,平台发现异常并介入使用了 8 分钟,随后 20 分钟进行了中文验证 码干预,效果比较明显,且无需人工进行干预,实现了快速全自动化响应,让扫号无所遁形。 问题:Sql+DB;数据量瓶颈;运算效率 三、技术驱动业务 携程的业务安全实践,通过业务驱动技术,解决了相关问题的同时,也带来了一些系统上的 瓶颈,并在业务的快速发展和攻击者的不断更新中,呈现了不少问题。为此,我们也正在寻 求一条新的道路:技术驱动业务,来更好的支持业务,从被动到主动。 1、新的系统架构 安全篇 252 安全篇 253 数据层: 数据层负责对各种结构化以及非结构化数据进行统一的数据收集,清洗,预处理操作。目前 来说,这种清洗注重点一般在区分正常用户和异常用户的注册,登录到账户各种重要操作, 浏览 PV 数据,到最后购买旅游产品的一个行为区别,以及用户存在是否批量操作的相关数 据抽取,这也被称为用户的社交网络区分。 规则引擎层: 规则引擎层负责将清洗及预处理完成的数据,使用实时流或者迭代作业按照定义好的规则或 者模型进行数据计算,将计算完成的数据,存放在数据仓库内,以供分析层或者应用层调用。 分析模型层: 分析模型层负责将目前已有的清洗完成以及计算完成的结果数据再清洗和归类,进行后续的 规则分析,补充,调整,模型的建立,以及离线+实时评分卡的数据权重比例调整等。 应用层: 应用层主要负责将综合得到的实时+离线的评分卡形式的得分结果通过 SOA 接口返回给业 务方,告知业务方请求是否存在风险,并提供操作建议,同时会将相关请求数据全部记录, 以供后续分析。 系统优势:  支持各类不同源数据方式,DB,api 接口,kafka 消息等  支持分钟级到年的离线海量数据计算,支持 storm 流式和 hive 离线方式  实现比 drools 更优化的实时规则引擎计算逻辑,支持实时配置及四则运算,评分卡等  高效的计算效率,千条规则毫秒级响应,整体接口内部平均响应 10ms 内  分层架构设计,灵活应用 spark,impala,presto 等新技术的引入  账户安全,反欺诈,反爬,接口防刷均可以使用规则引擎实现防护,能在各类场景应用 2、新的产品化验证码服务架构 主要涉及 bu 前端 js,验证码后端服务及 bu 后端,其中 bu 前端 js,验证码后端均是业务安 全开发,作为一个产品化的产品推广到 bu 后端接入,bu 后端只需要监听 js 事件及验证码 的校验,大大减少的 bu 的开发量工作,并简化了相关流程,风控和验证码在一个流程中实 现。同时 js 也兼顾其他相关功能,大大提高了安全防护能力。 安全篇 254 3、体验和安全性更好的滑块和选字验证码 在图片验证码体验的瓶颈及安全性防护的提升角度,安全开发了 2 类新的验证码:滑块和选 字验证码,从兼顾体验和安全性出发,同时也使用了上面的架构,引入了风控进行防护。滑 块在测试中,用户输入在 1~2 秒左右,大大提高了 4 位验证码的体验。选字验证码则兼顾 了一些其他很好的作用(保密),同时所有字体位置随机,且后端只校验坐标位置,大大提 高了破解难度,在体验上并不输于滑块,破解难度更高,成为我们后续的引入重点。 四、携程业务安全展望 携程未来在业务安全上还会继续应难而上,在技术和业务上共同发展,包括底层 spark, presto,impala 的引入,安全用户画像,惩罚中心,挑战服务的系统上线等等。 安全篇 255 携程作为国内互联网旅游企业的标杆,体验和安全的平衡一直是业务安全所追求的,在有了 技术支撑的前提下,这块也将进行持续的优化和改进。 同样,信息安全一直是全社会关注的话题之一,用户的安全感知,即如何提高用户自身的安 全思维和对信息的敏感,保护自己账户,信息和个人资产的意识,也是我们需要进行不断改 进所需要展示给用户的。 安全之路,任重而道远。 安全篇 256 图形验证码在携程的实践之路 [作者简介]闵杰, 携程信息安全部产品经理。2015 年加入携程,主要负责黑产防刷,验证 码,反爬以及 UGC 方面的产品设计,关注在低成本的前提下,解决以上场景的实际问题。 从互联网行业出现自动化工具开始,验证码就作为对抗这些自动化尝试的主要手段登场了, 在羊毛党,扫号情况层出不穷的今天,验证码服务的水平也直接决定一家互联网企业的安全 系数。作为 WEB 看门人,它不仅仅要做到安全,也要兼顾体验。 本文将分享携程信息业务安全团队在这几年里,对图形验证码服务所做的一些大大小小的改 变。各位可以将本文作为自身网站图形验证码搭建的小攻略,减少重复踩坑的情况。 1.0 时代 过去携程曾经使用了一套基于.NET 的图形验证码作为控制登录,注册,发送手机短信,点 评,重置密码以及其他相关场景的主要手段,目的也很简单,就是防止非实人的请求。 在这个阶段,设计时仅仅是满足了防御异常请求,并没有考虑太多用户体验以及产品上线后, 运营数据收集再分析改造的需求。 主要实现的功能点为:  图片验证一次失效  图片生成超时失效  支持生成 4 位和 6 位验证码,字符以英文和数字组成  支持简单的字符粘连,干扰线,干扰点,字体,字符大小,字体扭曲等配置。 验证码图示: 安全篇 257 流程图如图所示: 事后来看,这套验证码系统由于架构简单,接入简便,在很长一段时间内,担当了携程门户 主要的看门人的角色,尽管各大 BU 并不是非常情愿地使用了这套服务,但是在防范撞库, 恶意请求短信,批量获取优惠卷等场景下,还是体现出了一个验证码服务所应该起到的作用。 当时这套验证码服务上线后,遇到了不少的问题: 1、服务并没有对接入方做场景,事务等区别,整个携程的验证码难度都是统一的(除了部 分 BU 自己开发了验证码),经常发生在 A 页面出现被扫号情况,需要增强验证码难度,但 安全篇 258 是 B 页面随之反馈,验证码太难,收到用户投诉,需要降低难度,无法兼顾。 2、服务记录的字段仅仅能支撑服务正常运行,业务数据没有进行记录。导致了验证码有时 候调用和验证异常升高,完全无法知晓是谁在进行恶意尝试并进行拦截(比如封停恶意 IP)。 3、只有英文数字,并且字体单一,设置的扭曲干扰线简单点,极易被 OCR 识别,假如设置 的太难,就会造成连正常用户都无法识别的窘境,在很长的一段时间内,只能将难度设置在 简单-中等这种难度,谈不上对于批量 OCR 有任何的防御。 而说到这类验证码的破解,业内目前通用的方案,我了解下来有这么几种: 1、通过各种手段,绕过风控,或者绕过验证码,比如某些验证码是本地校验,有些验证码 的关键参数会下发至前端,用户可以修改参数达到控制验证码的目的,有些业务方接入了验 证码,但是没有把校验和业务接口绑在一起,导致人家可以直接请求业务接口,验证码形同 虚设。也有业务方在接入验证码的时候,登录动作先于验证码校验,也导致了验证码毫无意 义。这些在过往的接入过程中,都是实际发生过的情况。 2、ORC,从二值化,去除干扰线/点,确定字体区域,到最后识别,会根据验证码本身难度 的不同,显著的影响其识别率。 3、人工打码平台。目前打码平台针对图片验证码已经很成熟了,针对数字,英文,汉字, 智能问答都有不同的价格。 4、CNN 神经网络识别。通过大量的样本进行机器学习,对于某些 OCR 比较难识别的粘连 字符字体,可能会有一个较好的识别结果。 随着各类攻击越来越多,穿透性也日益增强,业务部门对于验证码难度需求不一致,以及自 身对于验证码数据收集再改造的这些情况下,改造这个服务已经成为了摆在眼前的事情。 1.5 时代 在总结了上述提到的这些问题以后,新需求就自然而然的产生了:  英文和字母模型太过单一,极易破解,需要加入中文字库。  业务数据保留,至少得知道威胁是哪些地方过来的。  能够在面对大量的 OCR 或者暴力尝试场景下,自动变化难度,降低对方的识别率。  区分接入的业务方,难度可以根据业务方做不同的调整。 在产品开发测试的一轮轮投入后,新的需求完成了,并且又花了将近一年的时间推广了各个 BU 接入了这个新的验证码。 (在这里说一个实际的情况,就是在第一版验证码开发的时候,并不重视保留接入方的信息, 导致了在进行第二轮验证码重新接入的时候,发现老的接入方有 3 位数,但是要找谁,完全 不清楚,以至于有些验证码到目前都没有进行更新,这个服务接入注册其实是一个很小的功 安全篇 259 能,但是在版本更迭,重大事务通知上,能起到非常重要的作用)。 新版本验证码比较好的解决了下面这几个实际场景面对的问题: 1、再也不会出现某个应用发现异常请求,刚刚调高难度没多久,就接到投诉电话,需要把 难度降低这种场景,可以根据各自应用手动或者自动调节难度。(原先是整个公司都是统一 的难度耦合在了一起)。 2、可以知道威胁是来自于哪个 IP 或者设备的,针对性的做出响应,比如封停请求。 3、在面对扫号配合 OCR 工具的情况下,这套验证码会自动不停的变换难度,干扰点/线, 字体,背景色,粘连度等,经过实际对比观察,防御效果比老版本提高了 2 倍,这里我们的 策略分为全局难度提高和针对纬度进行提高,比如假设只有一个 IP 或者设备的验证码请求 发现异常,我们只会提高来自于这个 IP 或设备请求的验证码难度,对于其他正常请求是无 感知的,但是在某些情况下,比如大规模的分布式扫号,可能你需要使用的就是全局难度提 高。 4、支持了中文验证码,实际测试,英文数字在成熟的 OCR 工具面前,哪怕做了混淆,解析 成功率也接近 50%,假设换成中文配合一定的混淆,解析成功率一般不高于 20%,这也是很 多团队初期,假设没有风控和其他的辅助服务,最直接,成本最低的提高防御力度的方案。 这套方案,作为主流的验证码方案在携程应用了很久,但是在去年,团队也终于意识到,还 有很多问题是这套验证码方案所无法解决的: 1、用户的体验问题,这个问题被公司内部诟病很久,并偶尔会收到来自于外部用户的投诉。 其实也很好理解,一个四个字的随机中文验证码,手机输入一次大约耗时 15-20 秒,这个在 活动营销拉新场景下,是一个致命的转化率杀手。在很多力度不是非常大的活动面前,这个 验证码会直接打消一部分正常用户去尝试的念头(尽管中文验证码在防御恶意请求接口上有 着巨大的安全优势),作为业务方产品不得不考虑,假设恶意用户的比例是 5%,牺牲剩下 95% 用户的体验是否值得,即便也只有 5%的正常用户放弃了尝试(比如一些年纪大的客户),和 恶意用户的比例相仿,但是剩下的用户还是需要输入这些验证码,在用户体验成本上的让步 是巨大的,这样就会让安全和业务陷入一种零和博弈的局面,这往往也是安全部门不受欢迎 的主要原因。 2、接入繁琐,验证码和风控作为 2 个服务,需要业务方去耦合,分别接入,听起来就很繁 琐,实际接入也确实很繁琐,大量的时间花费在接入服务上。 3、无法应对国际化,中文验证码国外用户不认识,英文数字过于简单压根没办法防御主流 扫号攻击。实际发生过的案例就是携程海外站点被大规模扫号,但是束手无策的局面,分布 式 IP 和设备,无法采用中文,几万个 IP 手动封不谈累不累,CFX 都装不下,只能看着他扫。 4、丑,验证码图片由于需要防破解的关系,往往和整体页面 UI 的风格完全没有一个搭配感, 让人很出戏。 安全篇 260 和各大竞品比起来,我们的验证码确实就像是石器时代在和工业时代比较,也被公司内部一 次次吐槽实在太影响体验了。在这种内忧外患的局面下,验证码服务更新又一次被放到了台 面上。 2.0 时代 需求又一次的被明确:  接入要简便,不要让业务方再需要自己对风控结果和验证码来处理相关逻辑。  体验要好,移动端时代,尤其要考虑移动端输入的习惯。  安全性和国际化问题,至少不能让有些用户投诉自己无法输入中文字。  美观,和页面 UI 尽量融合,也需要可以让业务方自行美化。 又是一轮一轮的测试开发,大约在半年前,完成了本次验证码重构的第一版,他主要实现了 如下功能: 1、体验问题被解决了,在对比多种竞品以后,我们采取的方案为“滑块+选字”,在安全认 为请求有风险的情况下,会出现滑块,假设在你滑动滑块期间采集的数据不足以认为请求是 可信的情况下,会出现选字或者直接禁止请求访问,根据实际数据,用户滑动滑块的时间耗 时一般平均在 0.5 秒一次,仅有很少的一部分用户会出现选字,选字一般的耗时在 7 秒左 右,平均下来,整体耗时目前在携程实践下来,在 1.7 秒左右。 2、接入问题也得到了改进,业务方目前接入仅仅需要在页面上引入一个 JS(APP 为一个 SDK),然后在监听到 JS 的一个事件提示,可以获取 token 后,获取 token,由业务方服务 端获取 token 以后到安全这里的校验服务校验,校验完成以后,整个流程就结束了,中间的 判断风险,出现滑块,滑块校验,出现选字,选字校验这些步骤业务方都无需干涉以及传送 数据,安全已经把后端的风控,风险仓库和验证码前端全部打通,业务方在无感知的情况下, 相当于接入及使用了 3 个服务。 3、国际化问题,目前已经支持东南亚多国语言的选字和提示语服务,常用国家再也不会有 无法看懂和无法输入的诟病。 新版验证码服务如图所示: 安全篇 261 安全篇 262 流程图如图: 这个版本作为目前携程验证码的主流应用版本,将在各个场景下代替过去的填字验证码,在 体验和安全性上,大幅度超越之前的服务。 按照目前携程的每日验证情况来计算,新验证码对比老验证码能每天给用户节约 500 小时 的验证时间。 填字验证码在 H5 端,通过大量实践,在保证安全性的前提下,输入正确率一般在 88%-90%, 存在了一个天花板,在新验证码上线后,整体通过率已经提高到了 96%,接近了我们认为的 实际异常比例。 同时我们对于前端采集的大量数据做了模型训练,对于某些规则难以发现的问题,会采用模 型的结果来进行处理。 在某些公共登录注册场景下,我们会采用 BI 画像+特定活动物料的模式,给特定的用户进行 验证码渠道的推广服务,如图: 安全篇 263 但是这套服务,也存在一定的问题: 1、滑块服务存在被破解的可能,据一些外部专业测试机构报告,目前外部主流的一些 SAAS 滑块服务,被破解的概率在 60%(他能让滑块认为他是安全的请求,选字就不会出现了) 2、选字的 OCR 识别依然存在,虽然存在要识别两套并点选的难度,但是有些外部专业团队 已经实现了一定程度的破解。 3、体验上,在 IOS 系统,滑块容易造成页面的回退,对于一些指甲长的用户尤其容易造成 这个问题,目前的解决方案只能是加大滑动条的大小,尽量远离屏幕边缘。 结语 没有一种验证码可以通吃所有场景,也没有绝对安全无法破解的验证码,只有在业务和安全 不停权衡的前提下,找到一个属于体验和安全平衡的点,这才是验证码正确的打开方式。在 和各种黑产团队不停斗争的过程中,验证码服务只有不停的改变,创新,才可以适应当前复 杂的黑产现状以及业务多变的场景。 安全篇 264 携程安全自动化测试之路 [作者简介]陈莹,携程信息安全部安全开发工程师。2013 年加入携程,主要负责各类安全工 具的研发,包括线上日志异常分析,实时攻击检测, 漏洞扫描等。 一、背景 业务代码上线前,通常会在测试环境经过一系列的功能测试。那么,从安全层面来说,web 应用常见的 web 漏洞,如 sql 注入,xss 攻击,敏感信息泄漏等,我们如何保证在上线前就 能够自动化发现这些业务的安全漏洞呢?本文将详细讲述携程安全测试的自动化之路。 二、技术选型 市面上也有很多各种各样的开源、商业扫描器。单就应用这一层来说,漏洞扫描器一般分为 主动扫描和被动扫描两种。 其中,主动扫描一般用于黑盒测试,其形式为提供一个 URL 入口地址,然后由扫描器中的 爬虫模块爬取所有链接,对 GET、POST 等请求进行参数变形和污染,进行重放测试,然后 依据返回信息中的状态码、数据大小、数据内容关键字等去判断该请求是否含有相应的漏洞。 另外一种常见的漏洞扫描方式就是被动扫描,与主动扫描相比,被动扫描并不进行大规模的 爬虫爬取行为,而是直接通过捕获测试人员的测试请求,直接进行参数变形和污染来测试服 务端的漏洞,如果通过响应信息能够判断出漏洞存在,则进行记录管理,有人工再去进行漏 洞的复现和确认。 所以我们可以发现,主动扫描与被动扫描最主要的区别为被动式扫描器不主动获取站点链 接,而是通过流量、获取测试人员的访问请求等手段去采集数据源,然后进行类似的安全检 测。 除此之外,基于主动扫描的 web 扫描器还有其他的不足:  由于数据源来自爬虫爬取,独立的页面、API 接口等就无法覆盖,存在检测遗漏情况。  如果是扫描单独的几个站点,主动扫描是够用的。但是在站点数量急剧增大的时候,主 动扫描的效率、精准、速度都无法与被动扫描相比。 最终我们选择基于被动扫描的形式去实现自研 web 漏洞扫描器。 三、架构设计 基于以上自动化的安全检测需求,由我们内部研发了 Hulk 项目,通过网络流量镜像等方式 来实现分布式的实时 Web 漏洞扫描系统。整个项目按模块可分为数据源,数据处理,漏洞 检测,漏洞管理等几大模块。 安全篇 265 如图所示,Http 请求数据从数据源发送至 Rabbitmq、Kafka 等队列。交由统计、去重、去静 态资源模块利用 redis 进行数据处理,处理后的 unique 请求存入 Rabbitmq 扫描队列,等待 scan engine 的消费。而 scan engine 则全权负责参数解析和变形,利用预先设置好的规则顺 序进行请求重放和漏洞检测。最终,如果 Scan engine 判断出某个请求含有漏洞,则落地到 MySQL 中,交由 Hulk 的运营人员进行漏洞的确认和复现。 四、数据源 数据来源主要有 2 种类型,即基于网络流量镜像的方式和基于 Http 代理的方式。 基于网络流量镜像的方式中,需要在办公网到测试环境核心交换机上做流量镜像,通过 dpdk、 pf_ring 等高速抓包模块进行流量获取,并按照五元组信息进行 tcp 流重组。然后通过 http 解析器,将其中 http 请求的请求方法、请求地址、请求域名、请求参数等数据提取成 json 格式,发送到 kafka 中。 当然,这其中还有一部分为 https 的请求,需要通过 rsa key 解密后才能交由 http 解析器正 常解析。随着 http2.0 时代的来临,大部分的 https 请求在进行秘钥交换时采用了更高安全 性的 Diffie-Hellman 秘钥交换算法,我们的 https 解密模块也逐渐退出历史舞台,只能后移 流量镜像模块,转向纯 Http 的流量捕获。 安全篇 266 基于 Http 代理的方式中,只需要配置代理服务器,将测试人员的测试请求数据收集起来, 然后采用同样的模块进行去重和统计处理,结果发送至 Kafka 队列,有着与基于流量的形式 同样的处理流程。 五、数据处理 流量进入到消息队列之后,去重模块会从消息队列消费,计算出 url、args 等的 MD5 值,在 redis 中进行去重,如果是一个已经扫描过的地址,则只记录一条日志到 ES 中;如果是一个新 的 URL 地址,就将其具体的 HTTP 请求发送至消息队列中,等待 scan engine 的消费。 在数据处理的时候,去重是非常重要的,这里涉及到不同请求方法、不同的参数,任何一点 不同,都可以被看重是不同的 URL 地址,也对应了不同的后端接口。 安全篇 267 除了这些之外,针对伪静态 URL, 我 们 也 需 要 将 /products/655554.html 归 一 化 为 /products/NNNNN.html。如上图所示,将数字归一化来去掉 30%左右的相似 URL。然后利用 redis 的 TTL 特性,使得一段时间之前扫过的 URL,可以在下一次的去重中被判断为新 URL, 从而再次加入扫描队列,等待新一轮的安全检测。 六、漏洞检测 扫描引擎从消息队列中读取去重后的流量数据,使用多种不同的方式去进行漏洞扫描。  一般的 web 漏洞配置规则来检查,比如 xss 漏洞, 文件包含漏洞,敏感文件读取等, 先替换参数,或重新构造 URL, 再重放, 再检查响应内容是否包含特定的信息, 以 此来判断是否存在漏洞;  sql 注入漏洞则使用高效的开源工具 sqlmap 来检测,避免重复造轮子;  另外还有一些其他漏洞,比如存储型 xss,struts 漏洞,ssl 的漏洞,这些无法使用简单 的替换参数重放的方法,但是我们提供了插件编写功能,这样可以让运营人员写插件, 以满足各种需求。 但是,从 storm 实时攻击检测系统过来的流量是不带 cookie 的, 如何扫描登录后漏洞呢? 我们生产 url 和测试 url 可以通过一种映射关系进行转换,保存各个测试站点的登陆信息文 件。当读取一个生产的 url 后,去获取它的测试地址和登录信息,就可以去扫描它相应的测 试地址了。这样就避免了影响线上用户。 扫描速度也是扫描任务的一个关键指标,在整个架构中,不同的模块之间是通过消息队列进 行数据传输的。所以当去重模块或者扫描引擎模块处理速度不够快,造成数据积压时,我们 可以通过增加模块实例来进行水平拓展。 七、漏洞管理 安全篇 268 对于扫描结果中存在问题的 URL 和对应漏洞,我们会进行一个快照功能。即将当时的请求 和响应包完整保存下来,方便运营人员验证漏洞。 且对于响应体内容,还可以进行一个基本的本地渲染,复现漏洞发现时的真实情况。 八、规则测试 同时,为保证规则有效性,我们还在管理控制台中集成了规则的测试功能: 安全篇 269 这样,只需要搭建一个带各种漏洞的测试环境,规则运营人员就可以在这里配置, 然后针 对性地对每一个规则、插件进行有效性测试。 九、总结 目前,整个项目上线稳定运行两年多,已发现线上高危漏洞 30+,中危漏洞 300+,低危漏 洞 400+,为线上业务安全运行提供了强有力的保障。当然, 对于数据污染、扫描频率、去 重逻辑、扫描类型等扫描器常见的诟病,我们后续也会一直不断进行优化迭代。 安全篇 270 携程业务风控数据平台建设 [作者简介]刘丹青,携程信息安全部高级开发工程师。2014 年加入携程,主要负责验证码、 风控数据平台的开发设计工作,提供性能测试与性能优化的相关支持。 前言 近几年,随着电商和互联网金融的发展,各大互联网企业也在逐步加强风控体系的建设,为 公司的运营保驾护航。在携程,各 BU 经常受到恶意注册、登录、恶意刷单、扫号等行为, 所以建设了一套数据平台,希望能够从数据中挖掘出有用的信息,不仅可以为风控系统提供 数据支持,还可以为其他服务提供支撑。 本文主要从架构和业务的角度介绍下携程信息安全团队的数据平台建设之路,以及如何为业 务和风控提供支持的。 一、数据平台 1.0 的特点 1.0 数据平台架构图 为了快速支持风控平台,在早期建设数据平台的时候,我们直接通过 RabbitMQ 收集业务数 据,再使用数据引擎对数据做清洗、计算,再存储在 MySQL 中,把数据处理以 sql 的形式 写入到代码中,通过高频的定时任务对数据做聚合统计。 在刚开始运行时,数据量和业务量都比较小,恶意攻击的手段也比较简单,所以数据统计还 是比较快速及时的,满足我们大多数的需求。 安全篇 271 随着业务量的爆炸式增长,数据处理的复杂度提升,我们不得不面对几个问题:  数据来源单一,并强依赖 RabbitMQ;数据量过大时,data engine 无法快速的处理完数 据,导致 MQ 中堆积的数据越来越多,最终导致服务器内存崩溃  做数据处理的 sql 语句都是写在代码里,通过 quartz 去调度定时任务,这样每当需要更 新数据处理逻辑时,不得不重新发布代码 二、数据平台 2.0 的改进 基于这两个痛点,无法提供稳定的数据服务,甚至影响了账户风控平台的业务,所以为了解 决这些问题,提供稳定可靠的服务,我们重新设计了数据平台 2.0,解决以上痛点,并从下 面三个方面考虑,解决以上痛点:  数据采集与整合  数据的实时计算与离线计算  任务调度与热更新 数据平台架构图 么接下来就谈谈我们具体是怎么去解决这些问题的。 1、数据来源 原来我们只能被动的接收风控平台传过来的数据,数据样本过于单一;在新版本中,需要收 集更多的数据,比如业务日志、行为日志、http 日志等;这些数据源位于各 BU 的存储上, 通过 Kafka 或者 MQ 流式的将这些数据拉取过来后,又由于数据格式各异,通过数据平台创 安全篇 272 建数据模型,并保存到 HDFS 存储上。 在风控的场景中,我们需要判断每一个请求是否是恶意攻击,与此同时,又不能影响普通用 户的正常体验,那么我们需要对所以的请求数据进行计算,并实时的给出响应,这个时间一 般都是秒级范围。 2、流式计算与实时计算 实时统计流程图 所以在这块我们做了两块计算,首先是用流式计算对数据做实时统计,通过 Storm 去消费 Kafka/MQ 的数据,并分发各种数据统计规则到 bolt 里,对数据做个初步计算,再使用 count server 对数据进行分片,同时进行数据流的继续处理。 通过 count server 得到的分片数据和最终的数据都会缓存起来,可以为接下来的实时计算提 供部分的数据支持。在这里,由于数据是通过 Kafka 或者 MQ 传过来,有时候可能出现数据 堆积的情况,导致无法进行实时统计,所以在这还做了一个请求-统计的超时监控,这可以 帮助我们及时处理数据流问题。 接下来是实时计算,由于实时计算的性能要求很高,所以当用户的请求过来时,在流式计算 结果的基础上做增量运算,最终达到一个实时的效果;这个结果也会存到 redis 中并定期做 持久化,可以作为下一次请求的参数,也方便后续的离线计算。 这里大概介绍下 count server,这个服务可以满足我们对于数据预热、分片存储的需求。 先举个例子:我们需要计算一个 IP 在 10 分钟内的访问次数,原来的做法就是通过索引日志 安全篇 273 或者直接去 DB 中查询 10 分钟内的数据,但是这样的效率还是比较低下的;我们通过 count server 把每个请求分别存储在一个时间槽里,当我们需要按照时间去统计的时候,直接获取 所有槽里的数据,并直接相加就能得到结果。所以这就可以看出来,count server 其实就是 按照各种维度,对数据进行分片存储。 3、离线计算 在离线计算这块,我们使用了十分流行的解决方案,Hadoop+Spark。随着数据的增长, MapReduce 和 Spark 任务的数量也逐渐的增加,并将计算结果按照不同的应用类型分别保 存到 MySQL、Redis、Hbase、ES 上。 4、任务调度与热更新 为了方便任务的调度与热更新,我们把任务拆解成任务单元和动态规则;在数据平台中,分 别创建任务单元和规则,通过 zookeeper 将规则打包成规则集推送到不同的任务单元上,实 现自动调度与热更新;这样即使规则出现问题,也不需要重新部署整个任务,只需要修改规 则并重新推送规则集即可。 同时为了更好的检查任务的正确性,还新增了测试单元,在测试单元中创建测试用例、设置 入参与预期结果,然后注入到任务单元中即可完成测试,这样可以极大的提高任务的上线与 更新效率。 5、效果 安全篇 274 伴随着新平台的上线,每天处理的数据达到近 30 亿,相较于原来的 1.0 版本,实现了近 30 倍的提升;而且拦截了大量的恶意攻击请求;并且整个平台的服务化之后,很大程度上减少 了开发人员的参与。 三、尾声 在建设数据平台的过程中,首先是考虑对业务的支持,脱离了业务空谈数据是没有意义的, 在对老业务提供支持的同时,积累经验,收集需求,为新业务提供快速的支撑;其次是平台 的扩展,随着业务的发展,数据量和数据分析也会要求的越来越多,数据平台不仅要可以快 速的横向扩展,还能在原有的数据链路上方便快捷的插入新的操作与功能。 毕竟数据平台的建设与运营并非一朝一夕的工作,也不是一个通过堆砌各种框架组件而成的 应用,而是根据自身的需求,合理的制定计划、设计架构,最终达到一个成本和收益的平衡。 安全篇 275 ES 安全 searchguard 落地实践 [作者简介]江榕,携程信息安全部高级信息安全工程师,目前主要负责公司运维安全日志分 析平台搭建、参与日常运维安全事件响应、参与运维安全评审。 一、背景 随着大数据技术在互联网行业的快速发展,应用于日志分析领域的技术趋向于多元化,而 elasticsearch 以其作为开源分布式搜索引擎所带来的诸多特点与优势,逐渐成为各家互联网 公司实时日志分析、甚至风控离线数据分析的主要战力。 然而在享受到 elasticsearch 带来便利和优势的同时,不可避免地存在将 elasticsearch 作为 db 用于存储敏感信息,由于 elasticsearch 本身不支持安全特性,带来极大的访问控制安全 风险。 为了对 elasticsearch 的访问控制进行安全加固,我们针对市面上的仅有两款安全插件进行调 研,由于 shield 为收费插件,故本文仅对 searchguard 开源插件的配置部署及落地进行详细 阐述,希望提供给各位一个快速上手配置 searchguard 指南。 search-guard 更新到 2.x 后与 shield 配置上很相似。 除了必须的 RBAC 认证授权外, searchguard 优点有:  节点之间的 RPC 流量通过 SSL/TLS 传输(强制性);  支持 JDK SSL 和 Open SSL(建议用 openssl,降低性能消耗);  支持热载入;  支持 audit 日志记录(商用功能);  支持 restful 接口流量加密(可选);  支持 ldap 认证接入(商用功能);  权限配置基本与 shield 保持一致;  索引级别访问控制(重点!);  字段级别访问控制(商用功能); 在我们考虑正式落地前,需要确认必要的安全特性,例如字段级别访问控制、内网 restful 流 量加密是否必要等。结合现有业务实际场景,最后选择仅做到索引级别的访问控制,且仅加 密 RPC 流量。 二、安装 当前配置的是 es5.1.1 对应的版本,故对应的配置文件安装如下: 安装 search-guard-5 安全篇 276 https://search.maven.org/remotecontent?filepath=com/floragunn/search-guard-5/5.1.1- 11/search-guard-5-5.1.1-11.zip bin/elasticsearch-plugininstall file:/// search-guard-5-5.1.1-11.zip 三、配置证书 下载: https://codeload.github.com/floragunncom/search-guard-ssl/zip/es-5.1.1 解压文件,进入 example-pki-scripts 目录。 注意,该步骤用于制作节点用的 truststore 及 keystore,故无需部署在生产节点上,创建完 成上述文件后部署到对应节点即可。另外,记得备份 example-pki-scripts 生成后的相关内 容。 创建 root ca 调整 etc/root-ca.conf 及 signing-ca.conf 中[ ca_dn ]内容,默认如下: [ ca_dn ] 0.domainComponent = "com" 1.domainComponent = "example" organizationName = "Example Com Inc." organizationalUnitName ="Example Com Inc. Root CA" commonName ="Example Com Inc. Root CA" 执行./gen_root_ca.shcapassword_use_a_strong_one truststorepassword 其中两个参数分别 为 ca 密码及 truststore 密码,请将该密码设置足够强壮。 该操作完成后生成 truststore.jks 创建 keystore 用于部署 data/master 节点执行命令 ./gen_node_cert.sh nodenumkeystorepassword capassword_use_a_strong_one 用于部署 client 节点执行命令 ./gen_client_node_cert.sh nodenamekeystorepassword capassword_use_a_strong_one 该操作完成后生成 xxxkeystore.jks 将各自生成的 truststore.jks 及 xxxkeystore.jks 上传至各个节点的 conf 目录下。 四、配置 elasticsearch.yml 安全篇 277 在 elasticsearch.yml 进行配置: searchguard.authcz.admin_dn: -cn=admin,ou=Test,ou=ou,dc=company,dc=com searchguard.ssl.transport.keystore_filepath:xxxkeystore.jks searchguard.ssl.transport.keystore_password:keystorepassword searchguard.ssl.transport.truststore_filepath:truststore.jks searchguard.ssl.transport.truststore_password:truststorepassword searchguard.ssl.transport.enforce_hostname_verification:false 重启 elasticsearch 服务。 五、配置文件 searchguard 主要有 5 个配置文件在 plugins/search-guard-5/sgconfig 下: sg_config.yml: 功能配置文件,配置包括认证授权都多种方式的功能,在本次 searchguard 部署中,除了增 添 kibana 认证外,其余不需要做改动。 sg_internal_users.yml: 本地用户文件,定义用户密码以及对应的权限。例如:对于 ELK 我们需要一个 kibana 登 录用户用于读,及一个 logstash 用户用于写: Kibana5: hash: $2a$12$.kM9IYaRm1uWeywD0Xyo8eynR6fAjVnHKrkxEAfIlZ1PeszTUovW6 #password is: kibana5 roles: - kibana5 logstash: hash: $2a$12$pLs.IeT/Ea8w81xQPqSRwOXah6h736UVjyvaHgXp9IjNjDxjQkKb6 roles: - logstash #password is: logstash 密码 hash 用 plugins/search-guard-5/tools/hash.sh 生成。 sg_roles.yml: 权限配置文件,这里提供 kibana4 和 logstash 的权限样例 sg_kibana5: indices: '*':#需要读权限目标索引,可通配 安全篇 278 '*':#目标 type,可通配 -'*'#具体权限 '?kibana': '*': -KIBANA_SERVER cluster: - cluster:monitor/nodes/info - cluster:monitor/health - CLUSTER_MONITOR - CLUSTER_COMPOSITE_OPS sg_logstash: cluster: - indices:admin/template/get -indices:admin/template/put -indices:data/write/bulk* -CLUSTER_MONITOR -CLUSTER_COMPOSITE_OPS indices: '*': #需要写权限目标索引,可通配 '*': #目标 type,可通配 -CRUD -CREATE_INDEX 关于详细权限,可参考 shield 的权限文档: https://www.elastic.co/guide/en/shield/2.1/reference.html#ref-actions-list sg_roles_mapping.yml: 定义用户的映射关系,添加 kibana5 及 logstash 用户对应的映射: sg_logstash: users: - logstash sg_kibana5: users: - kibana5 sg_action_groups.yml: 定义权限组,将同一行为目的的权限作为权限组,降低权限复杂度。权限组直接在 sg_roles 内角色定义中使用,很多权限组已有定义,如 KIBANA_SERVER 的子权限定义如下: KIBANA_SERVER: - "indices:admin/exists*" - "indices:admin/mapping/put*" 安全篇 279 - "indices:admin/mappings/fields/get*" - "indices:admin/refresh*" - "indices:admin/validate/query*" - "indices:data/read/get*" - "indices:data/read/mget*" - "indices:data/read/search*" - "indices:data/write/delete*" - "indices:data/write/index*" - "indices:data/write/update*" 六、加载配置并启用 在任意节点执行 sgadmin 命令进行配置同步: plugins/search-guard-5/tools/sgadmin.sh -cntestcluster -cd plugins/search-guard- 5/sgconfig/ -ksconfig/node-0-keystore.jks -ts config/truststore.jks -nhnv  testcluster 为 es 集群名。  其中 keystore 为该节点上的 keystore 文件路径,另外建议各节点的 sg 配置文件统一。  用户权限相关信息改动无需重启集群,仅执行上述 sgadmin 命令即可同步配置进 es 内 的 index。且仅需要在一台节点执行即可。 七、logstash 配置 在 logstash.conf 中作如下设置: output { elasticsearch{ user =>logstash password=> logstash ... } } 该设置中的用户名密码为 sg_internal_users.yml 对应用户 logstash 用户名对应的权限在 sg_roles.yml 中设置: sg_logstash: cluster: - indices:admin/template/get - indices:admin/template/put - CLUSTER_MONITOR - CLUSTER_COMPOSITE_OPS 安全篇 280 indices: 'logstash-*': '*': - CRUD - CREATE_INDEX '*beat*': '*': - CRUD - CREATE_INDEX 在 sg_roles_mapping.yml 设置对应的 logstash 映射: sg_logstash: users: - logstash sgadmin 命令加载配置。 八、kibana 配置 kibana.yml 内作如下配置: elasticsearch.username: "kibanaserver" elasticsearch.password:"kibanaserver" sg_roles 作如下配置: sg_kibana5_server: cluster: - cluster:monitor/nodes/info - cluster:monitor/health - CLUSTER_MONITOR - CLUSTER_COMPOSITE_OPS indices: '?kibana': '*': - ALL sg_roles_mapping 作如下配置: sg_kibana5_server: users: -kibanaserver 安全篇 281 sg_internal_users.yml 作如下配置: kibanaserver: hash: $2a$12$4AcgAt3xwOWadA5s5blL6ev39OXDNhmOesEoo33eZtrq2N0YrU3H. #password is:kibanaserver sg_config 作如下配置: authc: kibana_auth_domain: enabled:true order:1 http_authenticator: type:basic challenge:true authentication_backend: type:internal sgadmin 命令加载配置。 九、searchguard 集群性能测试 对于集群是否落地 searchguard,可能更多考量在与是否会对性能有影响。 从 searchguard 工作原理上说,加入 searchguard 会分别从认证、授权及节点间流量加解密 上有所消耗。对于性能强如物理机,认证授权这块可以忽略不计,但即使取消了 restful 的加 密,仍然有 rpc 流量需要加解密(如背景中所述,该加密功能不可避免)。为了验证影响, 我们采用了 esrally 进行相关测试。 测试方案为在两台物理机上各部署两个不同集群(有无 searchguard)的节点,分别用 esrally 进行测试,比对测试结果如下,baseline 为无 searchguard 集群,写入数据量为 280w 条, bulk size 为 5000,各位可以有个参考: Metric Operation Baseline Contender Diff Unit MinThroughput index-append 26188.1 19700.9 -6487.21094 docs/s Median Throughput index-append 59589.7 53227.7 -6361.99219 docs/s Max Throughput index-append 63968.8 57558.9 -6409.82812 docs/s 90.0th percentile latency index-append 699.846 882.144 +182.29781 ms 99.0th percentile latency index-append 797.955 1161.74 +363.78743 ms 100.0thpercentile latency index-append 802.317 1236.58 +434.26428 ms 90.0th percentile servicetime index-append 699.846 882.144 +182.29781 ms 99.0th percentile servicetime index-append 797.955 1161.74 +363.78743 ms 100.0th percentile servicetime index-append 802.317 1236.58 +434.26428 ms 安全篇 282 十、结语 由于能够支持 es 安全方案可选少,且 searchguard 这类开源方案对于性能上的影响需要时 间去验证(实际落地情况复杂)。对于多业务集群(存在多个角色进行集群使用且有访问控 制需求),可选择尝试 searchguard,即使该方案的配置有点麻烦。 安全篇 283 机器学习在 web 攻击检测中的应用实践 [作者简介]岳良, 携程信息安全部高级安全工程师。2015 年加入携程,主要负责渗透测试, 安全评审,安全产品设计。 一、背景 在 web 应用攻击检测的发展历史中,到目前为止,基本是依赖于规则的黑名单检测机制, 无论是 web 应用防火墙或 ids 等等,主要依赖于检测引擎内置的正则,进行报文的匹配。虽 说能够抵御绝大部分的攻击,但我们认为其存在以下几个问题: 1. 规则库维护困难,人员交接工作,甚至时间一长,原作者都很难理解当初写的规则,一旦 有误报发生,上线修改都很困难。 2. 规则写的太宽泛易误杀,写的太细易绕过。 例如一条检测 sql 注入的正则语句如下: Stringinj_str = "'|and|exec|insert|select|delete|update|count|*|%|chr|mid|master|truncate|char|declare|;|or|- |+|,"; 一条正常的评论,“我在 selected 买的衬衫脏了”,遭到误杀。 3. 正则引擎严重影响性能,尤其是正则条数过多时,比如我们之前就遇到 kafka 中待检测流 量严重堆积的现象。 那么该如何解决以上问题呢?尤其在大型互联网公司,如何在海量请求中又快又准地识别出 恶意攻击请求,成为摆在我们面前的一道难题。 近来机器学习在信息安全方面的应用引起了人们的大量关注,我们认为信息安全领域任何需 要对数据进行处理,做出分析预测的地方都可以用到机器学习。本文将介绍携程信息安全部 在 web 攻击识别方面的机器学习实践之路。 二、恶意攻击检测系统 nile 架构介绍 安全篇 284 图 1: 携程 nile 攻击检测系统架构第一版 首先我们简单介绍一下携程攻击检测系统 nile 的最初架构,如上图 1 所示,我们在流量进入 规则引擎(这里指正则匹配引擎)之前,先用白名单过滤掉大于 97%的正常流量(我们认为 如 http://ctrip.com/flight?Search?key=value,只要 value 参数值里面没有英文标点和控制字符 的都是“正常流量”,另外还有携程的出口 ip 流量等等)。 剩下的 3%流量过正则规则引擎,如果结果为黑(恶意攻击),就会发到漏洞自动化验证系统 hulk(hulk 介绍可以参考 https://zhuanlan.zhihu.com/p/28115732),例如调用 sqlmap 去重 放流量,复验攻击者能否真的攻击成功。 目前 nile 系统我们改进到了第五版,架构如下图 2,其中最重要的改变是在规则引擎之前加 入了 spark 机器学习引擎,目前使用的是 spark mllib 库来建模和预测。如果机器学习引擎为 黑,则会继续抛给正则规则引擎做二次检查,若复验依然为黑,则会抛给 hulk 漏洞验证系 统。 图 2:携程 nile 攻击检测系统架构最新版 这么做带来了以下好处: 1. 机器学习的处理速度比较快,能够过滤掉大部分流量再扔给正则引擎。解决了过去正则 导致 kafka 堆积严重的问题(即使是原始流量中的 3%也存在此问题)。 安全篇 285 2. 可以对比正则引擎和机器学习引擎的结果,互相查缺补漏。例如我们可以发现正则的漏 报或误报,手工修改或补充已有的正则库。若是机器学习误报,白流量识别为黑,首先想到 的是否黑样本不纯,另外就是特征提取有问题。 3. 如果机器学习漏报,那怎么办呢?按图 2 的流程我们根本不知道我们漏报了哪些。最直 接的想法就是并列机器学习引擎和正则引擎,来查缺补漏,但这样违背了我们追求效率的前 提。 最近的一个版本我们加入了动态 ip 黑名单,时间窗口内多次命中的的高风险 ip 重点关注, 直接忽略 storm 白名单。在实践中,我们借鉴了此部分黑 ip 的流量来补充我们的学习样本 (黑 ip 的流量 99%以上都是攻击流量),我们发现了 referer,ua 注入等,其他还发现了其他 逻辑攻击的痕迹,比如订单遍历等等。 有人可能会问,根据上面的架构,如果对方拿新流出的攻击 poc 来攻击你,只攻击 1 次,那 不是检测不出来了么?首先如果 poc 中还是有很多的特殊英文标点和敏感单词的话,我们 还是能检测出来的;另一种情况如果真的漏了,那怎么办,这时候只能人肉写新的正则加入 检测逻辑中,如图 2 中我们加入了“规则引擎(新上规则)”直接进行检测,经过不断的打 标签吐到 es 日志,新型攻击的日志又可以作为学习用的黑样本了,如此循环。 加入机器学习前后的效果对比:kafka 消费流量:1 万/分钟->400 万+,白名单之后的检测 量:1 万/分钟->10 万+。 我们设置了一分钟一个批次消费,每分钟有 10 万+数据从 storm 过来,只花了 10 秒钟左右 处理完,所以理论上还可以提高 5-6 倍的吞吐,如下图 3。 图 3:新架构下 spark 处理速度 我们先看一个机器学习的识别结果,如下图 4: 安全篇 286 图 4:机器学习 es 记录日志 rule_result 标签是正则的识别结果,由于当时我们没有添加 struts2 攻击的正则,但是由 ES 日 志结果可知,机器学习引擎依然检测出了攻击。 介绍了完了架构,回归机器学习本身,下面将介绍如何建立一个 web 攻击检测的机器学习 模型。而一般来讲,应用机器学习解决实际问题分为以下 4 个步骤:  定义目标问题  收集数据和特征工程  训练模型和评估模型效果  线上应用和持续优化 三、定义目标问题 核心的目标问题: 1. 二分类问题,预测流量是攻击或者正常 2. 漏报率必须<10%以上(在这里,我们认为漏报比误报问题更严重,误报我们还可以通过 第二层的正则引擎去纠正) 3. 模型预测速度必须快,例如 knn 最近邻这种带排序的算法被我们剔除在外 机器学习应用于信息安全领域,第一道难关就是标签数据的缺乏,得益于我们的 ES 日志中 已有正则打上标签的真实生产流量,所以这里我们决定使用基于监督学习的二分类来建模。 监督学习的目的是通过学习许多有标签的样本,然后对新的数据做出预测。当然也有人提出 过无监督的思路,建立正常流量模型,不符合模型的都识别为恶意,比如使用聚类分析,本 文不做进一步讨论。 没有一个机器学习模型可以解决所有的问题, 我们可以借鉴前人的经验,比如贝叶斯适用 垃 圾 邮 件 识 别 , HMM 适用语音识别。具体的算法对比可参考 https://s3-us-west- 2.amazonaws.com/mlsurveys/54.pdf 安全篇 287 明确了我们需要达到的目标,下面开始考虑“收集数据和特征工程 ”,也是我们认为模型 成败最关键的一步。 四、收集数据和特征工程 我们写段脚本,分别按天分时间段取 ES 黑白数据,并将其分开存储,再加上自研 waf 的告 警日志,以及网上收集的 poc,至此我们的训练原始材料准备好了 。另外特别需要注意的 是:get 请求和 post 请求我们分开提取特征,分开建模,至于为什么请读者自行思考。 一开始本地实验时,我是选用的 python 的 sklearn 库,训练样本黑白数据分别为 10w+条数 据,达到 1 比 1 的平衡占比。项目上线的时候,我们采用的是 spark mllib 来做的。本文为 了介绍方便,还是以 python+sklean 来进行介绍。 再来聊聊“特征工程”。我们认为“特征工程”是机器模型中最重要的一部分,其更像是一 门艺术,往往依赖于专家的“直觉”和专业领域经验,更甚者有人调侃机器学习其实就是特 征工程。你能相信一个从来不看 NBA 的人建模出来的 NBA 总决赛预测结果模型么? 限于篇幅,这里主要介绍我们认为项目中比较重要的“特征工程”的步骤: 特征提炼: 核心需求:从训练数据中提取哪些有效信息,需要这些信息如何组织? 我们观察一下 ES 日志中攻击语句和正常语句的区别,如下: 攻击语句: flights.ctrip.com/Process/checkinseat/index?tpl_content=&name=te st404.php&dir=index/../../../..¤t_dir=tpl 正常语句:flights.ctrip.com/Process/checkinseat/index?tpl_content=hello,world! 明显我们看到攻击语句里面最明显的特征是,含有 eval, ../等字符、标点,而正常语句我们 看到含有英文逗号,感叹号等等,所以我们可以将例如 eval 的个数列出来作为一个特征维 度。在实际处理中我们忽略了 uri,只取 value 参数中的值来提特征。比如上面的 2 条语句 flights.ctrip.com/Process/checkinseat/index?tpl_content 部分都被我们忽略了。 def get_evil_eval(url): return len(re.findall("(eval)", url, re.IGNORECASE)) 如果不存在 value,例如是敏感目录猜测攻击,那怎么办,我们的做法是分开对待,剔除掉 例如 flights.ctrip.com 等无效数据,取整个 uri 来提特征。 假设我们规定取 5 种特征,分别是 script,eval,单引号,双引号,左括号的个数,那么上 面攻击语句就转换为[0,1,0,0,2] 安全篇 288 最后我们得到一个攻击语句的特征是 5 维的,打上标签 label=1 ,正常流量 label=0 做区分。 这样,一个请求就转换成一个 1*n 的矩阵,m 个训练样本就是 m*n 的输入建模。 但是上线了第一版后,虽然消息队列消费速度大幅提升,识别率也基本都还可以,但我们还 是放弃了这种正则匹配语句的特征提取方法,这里说下原因: 1. 这样用正则来提取特征,总会有遗漏的关键词,又会陷入查缺补漏的怪圈 2. 优化特征较麻烦,例如加上某个特征维度后,会增加误报,去掉后又会增加漏报 3. 预测的时候,还是要将请求语句过一遍正则,转化为数字向量特征,降低了引擎效率 我们得到了使用机器学习来做情感二分类的启发,查证了资料 1 https://github.com/jeonglee/ML 后,决定替换掉正则提取特征的方式,采用 tfidf 来提取特 征。 我们认为本质上情感二分类和黑白流量分类是比较相似的问题,前者是给出一句话例如 “Tom,you are not a good boy!”来判断是否正面评价,而我们的语句中没那么多正面或负 面的情感词,更多的是英文标点和和一些疑似高危词语如 select,那我们概念替换一下,高 危英文标点是否就像是负面情感词,其他词就像是中性词,从而我们的问题就变成了二分类 “中性语句和恶意语句”。 这里简单介绍下 tfidf,更详尽的可以参考 https://en.wikipedia.org/wiki/Tfidf。 例如我们有 1000 条 get 请求语句,第一条语句共计 10 个单词,其中单引号有 3 个,from 也有 3 个。1000 条语句中有 10 条语句包含单引号,100 条包含 from,tfidf 计算如下(在进 行 tfidf 计算之前,我们需要对句子中的标点和特殊字符做处理,比如转为 string 类型,具体 参考资料 1): 安全篇 289 计算结果:单引号的 tfidf=0.587 > from 的 tfidf=0.3318 TFIDF 的主要思想是:如果某个词或短语在一篇文章中出现的频率,并且在其他文章中很少 出现,则认为此词或者短语具有很好的类别区分能力,适合用来分类。这里和我们的大脑判 断基本一致,单引号的 tfidf 值对比之下更大,比 from 更能代表一句话是否是攻击语句。 代码 demo 如下: 之所以取 ngram_range={1,3},是因为我们想保存前后单词间的顺序关系作为特征的一部分, 例如前面的“Tom,you are not a good boy!”中的一个维度特征是[not, a , good],然后计 算得到这个“集合词”的 tfidf 。当然你可以基于 char 来取特征,具体的参数取值宽度都需 要实验来证明哪一种效果最好。至于去停用词,标点怎么转换等等,大家可以参考 https://github.com/jeonglee/ML/blob/master/spark/NaiveBayes/src/main/java/WordParser.j ava,这里就不赘述。 样本数据清洗: 虽然我们已经明确了如何提取特征,建模貌似也 ok 了,这时我们问自己一个问题:训练数 据覆盖率怎么样,原始训练数据的标签是否准确?如果我们本身的训练样本就不纯净,结果 一定也不尽如人意。下面说一下我们在样本清洗中做过的工作: 1. 优化已有的检测正则:当打开 white.txt 和 black.txt,我们肉眼观察了一下,发现不少的错 误归类,所以说明我们的正则引擎本身就存在优化的需要。 2. 加入动态 ip 黑名单,收集其攻击日志,加入黑样本。经过我们观察,发现这种持续拿扫 描器扫描的 ip,其黑流量占比 99%以上 3. 关于白样本,我们可以直接按时间段取原始流量作为白样本数据,因为毕竟白样本占镜 像流量的 99.99%以上 4. 样本去重,相同请求内容语句进行去重 安全篇 290 5. 一些加密请求,根据参数名称,从样本中剔除 6. 自建黑词库,放到白样本去中去匹配是否命中词库内容,查找标签明显错误的样本。举个 例子,建立一个黑词库[base64_decode, onglcontext, img script, struts2....],然后放到白样本 里去查找匹配中的句子,剔除之。其实这种方法可应用的地方很多,例如旅游业的机器人客 服,就可以用酒店的关键词去火车票的样本中去清洗数据,我们也是受此启发。 特征清洗大概占我们工作量的 60%以上,也是不可避免的持续优化的过程,属于体力活,无 法避免。 特征归一化:由于这里我们采取了 tfidf,所 以这里就没有使用归一化处理了,因为词频 tf 就 带了防止偏向长句子的归一化效果。这里再提一下,如果用第一版正则取特征的方式就必须 使用特征归一化,具体原因和归一化介绍请参考 http://blog.csdn.net/leiting_imecas/article/details/54986045 五、训练模型和评估模型效果 初步评判 sklearn 训练模型很简单,这里我们交叉训练下,拿 50%的数据训练,50%的数据做 测试,看下效果是否符合预期。 如果此时交叉训练的结果不尽如人意,一般原因有 3 个,且一般是下列第一、二种原因导致 偏离预期结果较远,我们认为算法只是锦上添花,特征工程和样本的质量才是准确率高低的 关键。 1.特征提取有问题,这个没办法,完全基于个人特定范围的知识领域经验 2.训练样本有问题,错误标签较多,或者样本不平衡 3.算法和选取的训练参数需要优化 前面 2 个都介绍过了,下面我们讲一下参数如何优化,这里我们介绍使用 sklearn 里面的 GridSearchCV,其基本原理是系统地遍历多种参数组合,通过交叉验证确定最佳效果参数, 参考官方使用示例 http://scikit-learn.org/dev/modules/generated/sklearn.grid_search.GridSearchCV.html。 交叉训练达到心理预期之后,我们就将训练得到的本地模型存储到硬盘上,方便下次直接 load 使用。 训练和在线预测的 demo 代码如下,首先我们将黑白样本存储在 trainData.csv,分别存在 uri 和 label 标签下, 安全篇 291 图 5:训练样本数据 csv 存储格式 此时,如果用已知标签的验证数据来评估我们的机器学习模型,我们推荐使用混淆矩阵作为 评判标准, #expected 是标签值,predicted 是模型预测的结果 print("Confusion matrix:\n%s" % metrics.confusion_matrix(expected,predicted)) 输出: Confusion matrix: [[ 1 0] [ 4226 65867]] 大概解释下混淆矩阵的结果: 安全篇 292 由于此次我们的验证数据集只有 1 条正常流量,所以我们看到 FN 为 0 。我们更关心恶意流 量被识别为正常流量的情况(漏报),我们看到这里漏报达到 4226 条,如果要计算漏报率, 可以使用以下指标 print("Classification report for classifier %s:\n%s\n"% (model, metrics.classification_report(expected, predicted))) 输出: 召回率:Recall=TP/ (TP+FN) 准确率:Accuracy=(TP+TN)/ (TP+FP+TN+FN) 精准率:Precision=TP/ (TP+FP) , f1-score 是召回率和准确率的调和平均数,并假设两者一样重要,计算公式: f1-score=(2*Recall*Accuracy) / (Recall+Accuracy) 很明显,我们这里的召回率 0.94,代表我们的漏报率为 6%,勉强属于可接纳的范围内,还 需持续优化。 六、线上应用和持续优化 线上应用,也就是将建好的模型嵌入到我们已有的 nile 框架中去,且需要设置好一键开关机 器学习引擎,还有正则的一键开关,对于某些经常漏报的就直接先进正则引擎了,当然正则 个数需要约束,不然又走回了正则检测的死胡同了。后面我们就需要持续的观察输出,不断 的自动化补充规则,自动训练新的模型。 安全篇 293 参考前面提到的 nile 框架,目前遇到的最大的问题:我们如何面对遗漏了的攻击流量,是否 可接受这部分风险。目前还没有想到一个好的方案。 归根结底,我们还是认为特征提取是对模型准确率影响最大的因素,特征工程是一个脏活累 活,花在上面的时间远远大于其他步骤,对工程师的要求更高,往往要求大量的专业知识经 验和敏锐的直觉,外加一些“灵感”。 可以这样说,好特征即使配上较差的算法或参数,依 然可以获得较好的结果。因为好的特征就意味着离现实问题的本质更加接近。另外就缺一个 勤勤恳恳洗数据的工程师了。 七、未来展望 目前我们在机器学习方面的信息安全应用还存在以下可以更进一步的地方: 1. 对非标准的 json,xml 数据包的判断,因为这些数据中内容长,标点多,且有的是非标准 结构,例如 json 结构体无法顺利拆开,造成预测结果有误差。 2. 加入多分类,可以识别出不同 web 攻击的类型,从而更好的和 hulk 结合。 3. 在其他方面的应用,例如随机域名检测,ugc 恶意评论,色情图片识别等等,目前这方面 我们也已经陆续展开了实践。 4. 将 spark mllib 库替换为 spark ml 库。 最后一句话总结,路才刚刚开始。 运维篇 294 运维篇 运维篇 295 携程网基于应用的自动化容量管理与评估 [作者简介]陈剑明,携程技术中心网站运营中心数据分析高级经理,负责运维数据分析团队, 着重业务订单预测和基础架构容量管理。本文来自陈剑明在 GOPS 2016 全球运维大会上的 分享,首发于高效运维。 一、前言 今天跟大家分享的题目是基于应用的自动化容量管理与评估,简单解释下。容量管理与评估 相信很多同学都涉及过,根据开发人员的项目需求描述,或者压测数据,估计一个合适的服 务器资源需求。 自动化就是将这类评估决策做成一个自动化的进程不需要人为干预。而基于应用,是指我们 所有的容量评估最终目的是对应用的支持,而不是服务器本身是否健康,服务器本身很健康, 而上面跑的应用不健康,其实不算是好的容量管理。 上图背景中的很多问题是我们在日常工作会经常自省的一些疑问,在下面的讨论中我也会和 大家聊到这方面的一些思考。 二、如何提升资源的使用率? 对运营一个网站来说,业务规模会不断地增长,服务器数量会随之同步增长,然而资源的使 用率则会逐步下降。 因为一开始考虑的是生存,能实现功能,不至于频繁宕机就好,慢慢地随着业务增长会逐步 运维篇 296 开始关心性能,可用性,稳定性,考虑冗余/灾备等等方案,随之而来的,就是服务器数量 会增加,使用率逐步下降。 这就带来了一个疑惑,我们所采购的计算资源中,只有一小部分是真正地在帮我们做计算, 剩下的都是在凭空消耗。为什么会造成这样的情况呢? 究其原因,主要在于几个方面: 首先,追求应用的响应速度。像前一位演讲的同学提到的 CDN 会非常依赖响应速度,经常 有很多分析指出响应时间每提升 1ms,网站收入就能增加多少,所以就采用过量提供资源的 方式,哪怕是牺牲资源利用率。但是不是每一个应用都要无限追求响应速度呢?这里我们要 打一个问号。 其次,夸大资源需求。开发人员倾向于多申请一些资源以应对未来并不确定的变化。比如开 发需求的变更,或者对未来的业务量估计不足等等,需要给自己留一些 buffer。 最后,资源到位周期长。从提出资源请求,到最终拿到并发布好应用,这个过程需要等待多 长时间?2h?2d?2w?还是更长?等待的时间越长,开发就越是倾向于一次性多申请一些 资源。 运维篇 297 这些都是可以理解的,但却既费钱又不环保。IT 系统一向被认为是高效清洁的技术,但我们 却深知 IDC 其实是个耗能大户。 所以各大公司一方面通过 IDC 技术创新和服务器定制来降低能耗,另一方面通过提高利用 率来增加单位成本的效益。而我们今天要聊的就是后者,探讨如何在确保服务稳定的前提下 安全地提升利用率。 既要保障应用的服务质量,又要提高资源利用率,平衡好这两者的关系,是我们运维团队面 临的永恒的课题。 各位有没有大致统计过自家服务器的平均使用率?去年中科院计算所的一份研究报告指出, IT 公司的服务器平均使用率大概是在 12%。也就是说每投入 100 块钱在服务器和机房上,却 只有 12 块是真正用在需要的地方。 这么高的冗余程度究竟有没有必要? 以前我们在面对新业务对系统资源的需求时,因为潜在的风险厌恶,习惯性的思维总是倾向 于加机器,而另一方面,当我们可以把风险控制在一定范围之内时,适当提高现有资源的利 用率所带来的成本节省将是巨大的。 运维篇 298 初步估算一下,以目前携程的体量,CPU 平均使用率每提高 1%,节省下来的能耗和计算资 源,足以提供 13000 人一年的生活用电。携程目前的体量还不算特别的大,如果换成 BAT, 他们只要稍作提高,就可以节省很多能源。 所以,不管从经济效益还是社会效益来看,提高资源使用率这件事情都是有意义的。 显而易见,提高使用率也不是越高越好,那我们的度在什么地方?作为运维来说,我们的度 就在于满足上层服务的稳定性、可靠性的要求,这是我们的底线。只有在这条底线之上,我 们才可以去进一步考虑怎么来提高它的使用率。 运维篇 299 理论上讲,对于双 IDC 互为 DR 的系统来说,原则上使用率不超过 40%,即确保在任何情 况下,一旦某个 IDC 宕了,另外一个必须能够承担所有访问流量来支持应用的请求,这样 才能随时确保容灾。 三、提升资源使用率的度在哪里 基于这个前提以及业界同行的一些数据,我们认为实际使用率 25%以下为安全,超过 30%需 警惕,达到 40%则很危险,不管怎样都需及时扩容。而使用率不足 20%则是对资源和电力的 浪费。 所以我们试图提升整个网站资源的平均使用率。这里一个关键的前提是:控制风险。那怎么 来控制风险? 运维篇 300 首先我们承认一点,资源使用率并不简单地等于 CPU 利用率,也包括内存、I/O、参数配置 不当造成的软瓶颈等等。 而我们从另一个角度来看,所有这些资源的瓶颈最终都会表现为响应时间和出错率的增加, 所以不论系统有多少资源,我们总是希望能找到一个触及系统资源瓶颈的临界点,在这个点 之前,应用的性能表现和访问量是呈线性关系的,一旦访问量超过这个临界点,应用的性能 就会明显下降。 我们需要在确保不影响应用性能的前提下,最大程度地利用系统的各项资源。当我们找到拐 点之后,再来理性地选择是性能调优还是扩容。这就是我们做基于应用的容量管理的初衷。 运维篇 301 这是一个逻辑架构图画的比较简单,大概解释一下,就是在生产环境上,通过控制前端负载 均衡器上的权重,来控制流量分配,用生产环境的流量对集群中某一台服务器上的应用进行 压测,并在后台实时监控应用性能指标,任何一个系统资源到达瓶颈。 比如 CPU 连续几分钟超过一定的值,或者应用响应时间连续几分钟超过设定的时间,产生 性能拐点后立即恢复正常权重,整个过程除了被测应用各项资源使用率上升之外,其余影响 极小。 测试过程结束之后会自动搜集测试期间的性能数据进行分析,找到资源瓶颈,并计算被测应 用在当前资源条件下最大所能承载的访问量,以及整个应用集群所能承受的最大访问量。 这个是一个非常定制化的结果,因为不同的应用在不同的服务器上能跑出什么样的结果是完 全不一样的,所以我们会在内部所有的应用上跑到这样的测试,可以保证所有的应用在不同 服务器上它的极限值到底在什么地方,最后通过一些报表来展现。 这个是一个例子,我们在测试的时候会在同一个集群中随机选取两台服务器,其中一台作为 测试机,一台作为参考机,这里蓝色是测试机,红色是参考机。 我们会把流量往测试机上导,这里可以看到大概导 5 倍流量过去,随之而来的可以看到 CPU 的变化,测试机和应用机的对比,测试机从 20%上升到 60%左右,其实并没有达到瓶颈。 下面两个指标是应用非常关心的指标,一个是响应时间,我们可以看到响应时间在这样的压 力下其实两条线几乎是交错重合的,响应时间没有变化,所以这个应用在 5 倍访问量请求下 是完全可以承受的,包括后面出错率也基本上是零。 基于这样的结果开发人员可以非常明确地知道,我这个应用在 5 倍压力下是安全的,会有一 运维篇 302 个靠谱的心理预期。其实从这个结果来看,5 倍、6 倍甚至 7 倍都是有可能的。 压测过程中的所有性能数据我们拿到后台还要进行进一步加工和分析。比如这个图上,横坐 标是访问量 TPS,纵坐标是系统各项性能指标,我们给每个资源都设置一个阈值,当访问量 不断增加时,必然会有某一项资源最先达到瓶颈,我们用整个过程中的监控数据给 APP1 建 立一个回归模型,而这条回归线和阈值线的交叉点,就是此应用的最大访问量。 其实在携程很多情况是多应用混合部署,我相信在座的各位公司里面很多也有这样的情况, 一个服务器上不光有一个应用,可能有两三个应用,如果有其他应用其实也可以测到这样的 结果。同样的方式也可以给 APP2 建立一个回归模型。 多个应用之间的关系无非是相关或者不相关。所谓相关是一个应用访问量有波动,另外一个 应用也会跟着波动。无关是说一个应用波动不会随着另外一个应用波动而波动。 所以我们先看两个应用无关的情况,这种的情况很容易解决,把无关因素当成随即误差就好。 那如果相关的也是可以,我们分析这两个应用的相关程度,如果是高度相关的,那我们可以 用其中一个应用的访问量表示另外一个应用,同样可以做一个回归线。 现实情况是,CPU 使用率,或者说资源的使用率,往往同时取决于在服务器上跑的所有应 用,所以我们在这里就不能只用一个一次的回归模型做这个事情,我们需要用一个多元回归 模型拟合这样一个情况,解释每个应用对于资源的影响程度。 并且通过对单一应用和多应用同时压测的方式,拓展自变量的取值范围,增加模型的精确程 度。 运维篇 303 那对开发人员有什么帮助?其实我们服务对象就是开发人员研发人员,最大的帮助是可以帮 他们回答几个问题: 1. 他们会关心我的应用在生产环境上最大可以支持多少访问请求? 这个在我刚才的描述里面大家可以有这个答案了,就是我们用一个 TPS 的指标,来替换掉传 统概念中的 CPU 的指标,另外 TPS 这个指标背后已经隐含了各项资源的信息在里面,所 以他们只需要知道这个应用最多可以承受多少访问量就可以了。 2. 我的生产环境上了一些新的版本之后性能变好了还是差了? 对于当前版本代码在生产上的性能瓶颈一目了然,而且通过一次次的周期性压测结果,可以 看到整个代码版本推进过程中的性能变化。比如前一个版本应用的最大请求支持每分钟 200 个,而新版本上去后变成了最大 150 个,虽然功能上没有问题,但我们知道性能是下降了, 由此而来的,就是我们需要为此应用扩容以保持原有的容量不变,或者代码调优以恢复到原 有性能。 3. 业务说搞一些活动会带来多少倍的流量,我们系统能不能承受? 如果承受不住需要加多少服务器。这也是一个很常见的问题,我们做容量规划的时候,如果 有最大能承载流量的数值,回答这个问题就会非常的胸有成竹。不管业务上是需要支持多少 的访问量,我们都可以计算出一个既准确又能满足性能需要的资源需求。 4. 开发人员非常关心当前这样一个应用集群的容量使用是什么样的情况? 建立了模型,就可以随时反应容量的使用情况。应用的管理者可以随时掌握。 运维篇 304 四、开发和运维的协作关系 这里是我要跟大家聊的一个事情,开发和运维之间的协作关系。当我们对每台服务器上的每 个应用能承载多少访问量有了可靠的数据,并且结合当前和历史访问量进行分析的时候,我 们就可以判断一个应用集群什么时候需要扩容,除了提前扩容以应对自然增长的请求量之 外,突发请求量也能被捕捉到。 这时,当前容量够不够,不足的话需要增加多少台服务器,都可以马上决定,结合后续的 VM 自动上线流程和应用自动发布流程,就可以做到自动化的弹性缩扩容,大大缩短资源申请到 应用上线的速度。 我们当前的平均上线速度是一个多小时,而目前也正在优化各个环节,目标是做到 10 分钟 以内。也就是说,从开始申请服务器到新的资源投产正式分担流量控制在 10 分钟以内。 这样一来,容量管理就会变得自动而且高效,在提高资源利用率的同时,也让开发人员对自 己应用在生产环境的表现和瓶颈了然于胸。一旦对资源的保障也没有了后顾之忧,那之前提 到的导致使用率降低的核心因素也就解决了,形成了一个正向的反馈机制。整体运维成本也 自然就能随之降低。 那可能有些同学会有担心,我们没有给开发人员做任何的资源使用限制,会不会最后导致开 发不重视代码性能,资源被滥用? 从我们运维的角度来讲,开发人员就是我们的客户,他们需要资源,我们实际上是没有理由 驳回的,只能全力支持。那是不是意味着会被任意挥霍呢?其实不会,我们限制不了,但有 一双无形的手可以限制,对,就是财务。 大家可以注意到从我们反馈到开发的最后一步是成本分摊,使用的资源越多,运营成本也就 运维篇 305 越高,除非对成本没有要求,否则开发人员自己就会在代码性能和资源成本之间做取舍,这 也是个正反馈。没有理由说一个嵌套循环明明可以优化降低 CPU 的,却非要多申请一些资 源。 所以,我们通过这样一种方式进行资源的容量管理,可以逐步地将利用率提高到一个合理的 水平,并且保证应用不出问题,而且也可以对未来的容量需求有提前的预判和准备。 当然这一切的前提是你需要有足够的系统及应用监控能力,要达到自动化的容量评估,如果 有一点是必须要先做的,那就是系统层和应用层的性能数据监控和搜集。 否则以上的一切 都是空中楼阁,没有数据,所有都是空的。 所以,当我们在迎接智能运维时代到来的时候,要先想一想,数据是不是真的已经在搜集起 来了?我们的数据是不是足够大足够全,足以支撑起我们拥抱智能化的运维时代? 运维篇 306 携程运维工作流平台的演进之路 [作者简介]徐豪杰,携程旅行网技术保障中心流程工具团队资深软件工程师,于 2013 年加 入携程,主要负责携程工作流平台架构设计与建设,在流程建设方面有着比较丰富的积累与 经验。 前言 随着互联网技术的迅速发展,运维的事务也日益复杂,如何能更加高效的协调好人、工具与 流程之间的关系,把运维人员从低效率、高强度、易犯错的人工操作彻底解放出来,让他们 的能力与精力有更大程度的发挥,将是一个很大的挑战。 携程运维工作流平台在经过三年时间的演进,从最开始引入商业产品 BMC Remedy ARS 做 为底层单一工作流引擎,慢慢演化到抽象工作流平台的建设并扩展支持更多开源的工作流引 擎,同时把原来的平台进一步分层、建立了统一标准的接口,对业务进行了服务标准化、业 务流化以及流程自动化的改造。 本文将从以下几个主要方面分别阐述流程平台的建设与演进: 面临的挑战 运维工作流平台的建设 收获总结 下一代流程平台的设计 未来展望 一、面临的挑战 在讲挑战之前,我们先简单看一下携程运维工作流平台的演进历史: 运维篇 307 流程平台的演进主要分为以下三个阶段: 第一阶段:摸索期 从 2013 年下半年到 2014 年上半年,我们引入了 BMC Remedy ARS 产品作为携程 IT 服务管 理工具,但是我们发现它的基于工作流工作的思路可以借鉴到更复杂和核心的运维流程中, 所以开始尝试使用它的底层引擎构建我们自己的流程。 第二阶段:成熟期 在 2014 年下半年到 2016 年上半年,在 Remedy 平台上相继开发了一系列的流程,包括: 服务器上线、下线,应用上线、下线、扩容、缩容、迁移等等,还包括 ENP,API gateway 等 一系列标准化的接口服务模块,开始渐渐形成流程平台。 第三阶段:革新升级期 在经过了一个成熟期之后,现有的流程也慢慢暴露出了一些新的问题,包括流程的可视化、 价值数据的挖掘、底层流程引擎的单一,为了解决这些问题,我们从去年下半年开始,对原 来的流程进行了重新设计,抽象出更加标准化的流程模型,以及标准规范的可视化和监控。 那么在没有流程平台之前,我们到底面临着怎么样的挑战呢? 运维篇 308 在这里我给大家举一些运维过程中常见例子,比如日常工作,当用户碰到问题时 IT 部门报 障时,用户如何跟踪问题处理的进度,又或者当网站发生故障时,如何能够快速恢复服务, 后续的问题分析是否有流程支持,运维人员的日常变更操作,是否有过风险评估以及审批授 权,我们又是如何保障配置数据的可靠、准确性,服务器如何上下线,应用生命周期有没有 统一的管控,等等,正是在这样一个背景下,我们开始着手设计建设一个综合的工具平台来 统一管理运维过程中涉及的这些流程。 二、工作流平台的建设 关于自动化流程平台建设,自动化的意义相信大家应该都是有共识的,在上面的挑战中我们 已经提到了这些运维过程中大家可能会碰到的问题,那么我们是如何去建设这个流程平台的 呢? 1、系统架构 我们先来看一下最初设计的流程平台系统架构 运维篇 309 从下往上看主要有三层: 底层工作流引擎 前面我们已经提到了,在一开始,我们引了 BMC Remedy 产品主要作为内部 IT 服务管理流 程的支持,在原有的 ITSM 包括事件、问题、变更以及请求单基础上做了携程自己的一些客 户化定制,同时在这个平台上设计开发了包括服务器上、下线,应用相关的上线、下线,扩 容、缩容等支撑业务的相关流程。 中间接口网关层 在中间接口网关这层从左往后分别是 OSG 服务网关、ENP 事件通知平台以及协助工具,为 什么会有这样的设计呢,主要有以下几点考虑? 第一,外部工具服务过多,需要统一标准化管理 需要对流程接口服务进行标准统一的管理,所有工具提供的服务需要注册在平台; 第二,服务访问权限的控制 可以通过平台对提供服务进行权限控制,可以查看提供的服务由哪些外部用户或者工具调 用,同时申请了哪些外部工具的服务,可以对申请进行授权 运维篇 310 第三,工具提供的服务质量 可以通过可视化的前端页面查看当前提供的服务的质量,比如服务的响应时间 第四,访问日志 当进行问题分析排障时,可以有被追踪的日志 第五,产品局限 现有解决方案提供的接口只支持 JAVA 以及基于 Web Services 的 SOAP 协议,无法提供很好 的扩展,所以需要在这层做些接口的包装,以支持更主流灵活一些接口协议,比如 RESTful。 上层外部工具服务 最上面就是需要来访问流程的所有外部工具服务。 2、标准服务网关 – OSG OSG 是接口网关的核心的组件之一,那么什么是 OSG 呢? OSG,全称:Open Service Gateway,开放的标准服务网关,外部工具作为服务提供者可以 将他们的服务注册在 OSG 网关,其它工具若需要消费可以找到对应的服务,以服务消费者 形式申请某项服务的访问权限。 运维篇 311 当服务与服务之间相互交互的时候,OSG 就充当着路由的角色,对访问的请求进行转发, OSG 另一个主要功能就是流控熔断机制,可以为服务设置每分钟最大访问次数,当最大的 访问次数超过设定阀值时,服务自动设置为 Blocked,直到服务提供者重新启用。 对于工具之间服务相互访问日志都会被集中采集并吐到后台 ES 服务器,用户可以通过前端 可视化的界面对服务的质量、工具的访问进行实时监控、同时可以根据日志进行问题排查。 运维篇 312 3、流程与外部工具的桥梁 – ENP ENP – Event Notification Platform(事件通知平台),这里大家可能会有这样的疑问,这个不 就是消息队列吗?可以这么说,事件通知平台设计参考消息队列,但实现上又稍有些不同, 之所以会有这个平台,是因为从 Remedy 产生的消息数据在处理上有些局限,所有的数据的 格式化处理都需要交给 ENP,由 ENP 根据设置规则进行封装处理后再转发给外部工具。 为什么需要这么一个平台呢? 刚开始 Remedy 平台上开发第一个服务器上线流程的时候,整个上线流程中涉及到了多个 环节与外部不同工具交互,不同工具之间的接口实现非常复杂,所以在流程从半自动化到全 自动化的转化过程中,开发人员花费了大量的时间与精力与工具进行联调,而且不同接口在 实现方式也不统一,比如有些是基于 Soap 的 Web Service,有些是 RESTfulAPI 等等,另外 通过这样一个平台可以降低工具之间的耦合度。 举个场景: 我有一个应用,调用了很多其它服务,其它服务有时会发生异常,为此又不想花太多精力去 实现与维护。 我希望: 及时通知我 运维篇 313 及时通知服务提供人 能看到异常发生频率、次数、时间 能看到异常的内容 能看到所有通知事件 那么平台是如何运作一个消息从产生到工具的传递呢? 1.首先需要在平台注册一个事件 2.工具访问之前需要在平台上搜索该事件到并且订阅,需要提供一些必要的信息,比如工具 服务的调用地址 3. ENP 根据规则对消息进行格式化并封装 4. ENP 转发消息到外部工具 5. 具回写消息到 ENP,ENP 最终回写到流程平台 4、流程场景:服务器上线 下面我以服务器上线流程具体场景为例进行说明如何设计流程: 在经过前期与各个业务部门,运维团队一起分析设计出来的最终服务器上线流程图,流程由 一系列流对象组成,这些流对象可以任务,也可以是子流程,比如下图中所示的子流程“虚 拟机入池”,同时这些任务与子流程之间由连接对象衔接,比如顺序,并行,分支判断处理。 流程还需要设定了一系列的业务规则,包括审批、状态迁移、SLA、CMDB 数据落地等等。 运维篇 314 5、可视化的流程运行实例 在有了流程之后,对于不同角色的用户,比如运维人员、开发人员、流程组以及管理人员, 需要有个可视化的界面: 能清晰直观的看到所有运行的流程运行 能清楚了解当前某个流程运行或者等待在哪一个环节 流程的运行状态、时效 下图就是流程平台可视化界面,可以看到目前所有运行中的流程实例,它们的运行状况,流 程的运行时间,不同流程的运行数,当某个流程实例运行超过预定 SLA 时长,也可以查找超 时子单,并找到相应的责任团队。 运维篇 315 通过打开单条实例数据,可以进一度查看实例详情,在此可进一步看到所有运行或者已经完 成的任务以及它们的处理时间,一旦任务处理异常挂起时,可以通过查看任务的责任团队迅 速找到相关责任人进行快速处理。 三、收获总结 1、10X 服务交付能力的提升 运维篇 316 2014 年当时还没服务器上线流程时,业务部门从提出需求到最后交付平均需要一周左右的 时间,最长可能会达到二周,所有上线的相关环节的协同操作几乎都由低效率的人工完成, 很少有工具参与自动化处理。 有一个场景让我至今映像深刻,当时整个网站运营中心的各个运维团队座位比较近,所有每 当有上线的时候,经常会听到不同团队之间隔空对话方式进行沟通,还停留在通信基本靠吼 的原始时代。 现在有了流程支持,这种低效率的团队合作方式也大大得到了改善,各个运维团队开始尝试 开发自己的工具并接入到流程,流程也慢慢的从部份工具的接入的半自动化到所有工具接入 的全自动化处理模式运行,在流程完全自动化后,最终上线效率得到了大大的提升。 2、服务流程标准化流程化到自动化 运维篇 317 工作流平台的建设过程主要也是经过了以下几个阶段的发展, 第一,服务标准化, 前面我们已经讲到了关于服务标准化,就是把现有的平台进一步分层, 建立标准接口,像 OSG 这种的标准化接口网关; 第二,业务流程化,梳理各项业务,把大部分的 IT 运维工作流程化,确保这些工作都可重 复; 第三,流程自动化,把运维人员从低效率、高强度、易犯错的人工操作彻底解决出来,让他 们的能力与精力有更大程度的发挥; 在经过成熟期后,现有的流程平台也逐渐暴露出一些问题,最主要的就是过度依赖单一的底 层流程引擎,我们希望能够扩展更多的开源流程引擎,多个流程引擎之间可以随意切换,最 终可以完全替换掉原有的商业引擎,如果需要支持更多不同的流程引擎,那么对流程进行抽 象是必不可少的,建立出一个新的流程模型。另外一个问题,原来的流程引擎只能处理串行、 并行以及简单的分支合流,新一代的引擎可以覆盖到所有复杂的流程分支处理,接下来的章 节我们会讲到,如何设计处理复杂的场景。 运维篇 318 四、下一代流程平台的设计 1、改进后的系统架构 我们再来看一下改进后的系统架构 运维篇 319 可以对比一下原来的系统架构,大家应该能发现他们之间的主要区别,底层除了原先的 Remedy 外,可以引入其它的开源流程引擎,比如基于 BPMN 标准的 Camunda,或者 Activiti、 Airflow、Stackstorm 等等,中间也是最核心的就是抽象工作流层,主要以几个模块组成: 1)适配器 主要负责不同流程引擎之间的数据映射交互 2)基于 BPMN 标准的模块,包括仓库、运行时、历史、身份 仓库(Repository)主要存储流程的定义以及部署信息,所有流程执行过程中产生的实例、 任务实例以及变量实例的数据会存储在历史(History)模块中,运行时(Runtime)模块主 要负责所有正在运行中的用户任务,执行实例以及定时作业等,最后身份(ID)模块是用于 存储用户、组以及他们之间的关系 3)接口网关 主要负责与原来的接口层进行数据之间的交互,并实现与外部工具服务的通信 前端由自研的 Mario 运维工作流平台做为核心数据的可视化集中展现,监控报表等等。 2、可视化 除了原来对流程实例以及实例详情进行可视化管理外,底层的其它有价值数据并没有得到深 运维篇 320 挖,比如跟业务相关的数据,各个业务部门关心的历史服务器、应用的趋势等,那么通过新 的可视化我们可以更清晰看到业务部门的相关指标数据以及历史趋势。 3、监控告警-EITS 流程执行过程的异常由监控告警平台 EITS 负责处理,通过这个平台我们可以配置告警策略 以及不同报警的分类,一旦流程执行异常时,告警会通过邮件方式提醒用户处理,用户可以 登录到平台查找到对应的告警进行问题处理。 运维篇 321 4、支持复杂流程 前面我们提到了,原来的流程引擎只能处理串行、并行以及简单的分支合并,在新一代流程 平台我们做了一些设计调整,可以覆盖所有流程场景,我们以下图中最后面的一张图为例进 行详细说明: 1) 任务 a1 处理完毕后,同时产生三个并行的分支任务 2) 分支一由排他性的网关处理,根据中间产生的不同数据进行条件判断,如果满足条件则 执行任务 a2,若不满足,则执行 a3,排他性网关只会有一个任务被执行 3) 分支二由包含性的网关处理,任务 a4 如果满足条件则被执行,a5 始终会被执行 4) 分支三只不包括任务网关,任务 a6 会与分支一、二并行处理 5) 待所有分支处理完成后,流程合并 6) 继续处理串行任务 a7 运维篇 322 五、未来的展望 最后就是我们对未来流程平台的几点考虑, • 智能化 目前告警种类过多,很多流程上的异常告警,在经过人工分析后,多数是不需要处理的,我 们的目标是系统能做一些基础的分析,并在自动诊断问题后,对问题进行自我恢复,对于有 些需要人工介入的,也能做些初步的分析,同时把经过分析后的日志发送给处理人员,这样 可以提高处理人员的效率。 • 自助式的流程编排 目前的流程的开发还是需要流程团队参与,我们期望在未来,所有的用户可以自行编排流程, 自行定义流程中的每一步工艺,所有的工艺可以发布到一个共享的市场,当用户需要编排流 程的时候,除了定义自己的工艺外,也可以引用其它团队已经写好的工艺。 运维篇 323 携程新一代呼叫中心话务监控平台 [作者简介]通信技术中心,主要负责携程呼叫中心日常运维,包括配置管理和监控平台开发, 目前主要在呼叫中心运维自动化方向探索和演进。 一、携程呼叫中心话务概况 携程作为中国最大的 OTA,和国内外近十家电信运营商展开合作,目前拥有语音线路通道 10000+,包括传统语音线路以及基于软交换平台的 SIP 线路,每天的话务量更是以百万计。 从业务类型来说,又可以分为人工呼入呼出、自动呼入呼出和自动转呼等等。 面对不同运营商、不同线路特性的运维管理和灵活多变业务需求,基于监控精细化、自动化、 操作便捷化标准下做到对故障快速响应和处理的目标,我们开发了一套针对呼叫中心话务监 控管理平台——Horus 系统。 二、原有监控痛点 携程呼叫中心原先有一套监控携程所有的呼入呼出话务的监控系统,不过在使用过程中,系 统存在以下问题: 运维篇 324 图 1:原有监控痛点 三、Horus 系统特点 Horus 系统对这些问题进行了针对性的处理,目前通过以下功能可以自动发现并及时准确的 进行告警: 图 2:Horus 系统特点 运维篇 325 四、Horus 系统架构 系统架构图: 图 3:系统架构图 Horus 系统主要由以下模块组成:  采集模块:从数据源采集数据,输送到消息中间件 Hermes,支持 DB 及 API 方式采集;  存储模块:从 Hermes 获取数据,并存储到文件数据库 Hickwall,将监控项索引存储到 Mysql DB 中;  故障检测:从 Hermes 获取已采集的数据并调用数据分析模块进行故障检测;  数据分析:根据历史数据分析生成告警规则,检测当前数据,生成检测结果和告警信息;  告警聚合:将告警进行聚合,聚合后的告警信息返回 Hermes;  告警通知:从 Hermes 调取告警信息,执行告警通知,并将告警通知存入 DB;  自动测试:调用“语音机器人”功能,执行自动测试任务;  监控展示/配置:执行用户和系统的交互; 系统逻辑图: 图 4:系统逻辑图 五、Horus 系统解决方案 整体方案主要结合以下几个方式进行设计: 正态分布 运维篇 326 周期属性 差分统计 特征分析 数据关联 自动测试 自动检测 算法原理: 自动检测是 Horus 系统的核心功能,我们的做法是对海量历史数据进行采集和处理,按照以 上几个方案形成 3 种智能化检测策略。 1. 阈值分析 将历史数据结合正态分布生成阈值上下限,再计算越界次数,生成阈值分析策略。为了提高 阈值准确性,我们将历史数据区分工作日、双休日以及节假日。同时考虑周期属性,将数据 按时间片再细分,对比每天同时刻数据,缩小偏差。 2. 变化率分析 根据数据变化的趋势,利用差分统计计算前后点之间的变化率,和自身数据前后趋势作比较, 生成变化率分析策略。 3. 跌零检测 对当前数据进行跌零检测,结合损失话务量和跌零次数判断是否告警。 4. 自动告警逻辑: 根据以上三个策略对实时的监控数据进行检测: 1)先进行跌零检测,如判断数据跌零且满足累计损失话务量或次数条件,则告警; 2)如果数据未跌零,则进行阈值分析和变化率分析,部分场景再结合累计影响话务量以及 是否为节假日判断,满足条件则告警,否则不告; 阈值分析&变化率分析示意图如下: 1、Point-A1 的值超过阈值下限,且该点与前一点的变化率 C-Rate1 大于变化率门限,所以 该点为异常点。 2、Point-B1 的值超过阈值上限,但该点与前一点 Point-B2 的变化率 C-Rate2 小于变化率门 限,所以该点判断为正常点。 运维篇 327 图 5:阈值分析&变化率分析示意图 六、业务应用场景: 话务量监控 成功率监控 周期性特征取值 小话务量的离散数据 关联数据告警 长期小幅下跌 话务量自动检测: 某号码话务量在一个时间段数据陡降,连续 2 个点低于阈值下限,同时变化率大于门限值, 触发告警。 运维篇 328 图 6:话务量告警 成功率自动检测: 成功率的检测不仅仅是检测是否低于某固定指标,也是需要根据不同业务的特性以及不同时 间段进行监控。针对容量类或成功率等特殊监控类型,我们基于历史数据进行阈值分析,生 成动态的阈值上限或下限规则,即能满足告警要求。 图 7:外呼成功率告警 周期性特征取值: 因业务类型不同,部分话务数据存在陡升陡降情况。针对此类数据,我们采用特征分析的方 法,自动发现其特征规律,减少误告。 例如某监控项在每周固定时间段有一个数据突升,我们通过特征分析发现了这个规律,作为 后期告警检测时的重要参考,避免误告。 图 8:周期性特征取值 小话务量的离散数据 运维篇 329 小业务量监控项一直以来都是业务监控的盲区,因为业务量小导致告警规则难以确定。对此 我们采用了数据聚合的策略,从数据量和业务形态两方面入手,自动分析监控项特征并打上 标签,通过不同聚合维度对监控项进行检测和告警。 图 9 的常规时间序列图中,该监控项的数据是离散的,传统方法无法有效监控起来。经过聚 合之后,图 10 的数据被聚合成 1 小时维度的,这样形态就变得很有规律,可以进行检测和 告警。当然,聚合之后虽然可以解决检测和告警的问题,但展示和监控维度都变成 1 小时, 从问题发生到告警触发时延有所延长。 图 9:常规时间序列图 图 10:聚合时间序列图 关联告警 话务监控项之间往往存在着一定的关联性,我们通过将 2 个或多个相关的监控项自动关联, 以减少误告避免漏告。 下图我们将传真请求总量、传真发送总量进行关联。以下红色圆点请求总量增加,但发送总 量没有变化,因此进行告警,而后面的请求总量增加,同时发送总量也增加,系统关联后, 不进行告警。 运维篇 330 图 11:关联告警 长期小幅下跌 对于某些长期小幅下跌的问题,传统方式无法有效检测,通过聚合之后阈值和累计影响话务 量的检测,可以有效发出告警。 图 12:长期小幅下跌 七、告警筛查 告警检测依赖的是历史数据分析,而业务也有其随机性,因此不可避免的存在误告的可能。 对此,我们采用自动测试方式对告警进行筛查。 自动测试功能的实现基于我们的另一款产品“语音机器人”。针对话务量,成功率等监控项, 获取疑似告警发生后,系统会调用“语音机器人”功能,根据测试用例进行自动测试,将测 试结果返回后插入到告警信息中,并告知运维负责人该告警为误告。 运维篇 331 图 13:自动测试报告 八、告警聚合 监控系统发出告警之后,可能会引申出另一个问题,那就是告警风暴。告警风暴通常发生于 以下两种情况: 1、系统发生大型故障,多个监控项同时发生告警 2、单个监控项连续发生告警 针对告警风暴,我们的做法是将大量重复的、或同类的告警压缩为一条真正有意义的告警, 为运维人员提供甄选之后的最重要的告警。 这里有两个规则: 1、同一通知组,1 分钟内同时发生的不同告警合并成一个通知内; 2、同一监控项,30 分钟内的告警只通知一次,不再重复通知; 用户也可以查看聚合之前的告警以及他们的时间序列关系。实际使用中,告警压缩率可以达 到 95%以上。 九、其他功能 便捷接入 提供 API、DB 和插件等多种方式,3 步完成接入。 Step1:配置采集信息,包括监控对象分类、采集名称和采集方式等 运维篇 332 图 14:采集配置 Step2:自动校验,生成监控项 图 15:生成监控项 Step3:确定图表需要接入的菜单名称,同时完成启用告警、告警通知组等全局配置 图 16:全局配置 可视化编辑 图表编辑页面,可同时完成图表命名、监控项选择、添加上周曲线、添加基线、修改颜色和 编辑告警规则等典型场景下的常用操作 运维篇 333 图 17:可视化编辑 运维大屏 梳理各类运维数据,投放到大屏上进行综合展示 图 18:运维大屏 自动语音通知 对于告警通知,我们在邮件通知的基础上增加了电话通知功能。在告警发生后,调用“语音 机器人”模块向系统负责人手机发起呼叫,通过文本转语音模块将告警内容自动转化成语音 及时通知应用负责人。运维人员在听取告警内容后,如暂时无需处理或暂时无需关注,可通 过按键进行“屏蔽”或“误告”等操作,避免系统一直告警。 运维篇 334 告警通知采用自动升级机制,如三次系统负责人不接听电话,自动升级至其主管,如主管不 接电话,自动升级至更高级别管理人员。(升级机制可通过参数配置) 十、后续规划 Horus 系统投入生产后,接入的监控项 type 已达 17 种,不同的业务类型引出了更多的问题 和需求。后续我们会继续通过大数据分析、AI 等人工智能技术不断优化监控平台,并在用户 界面提供更多便捷、个性化的操作体验和展示效果。当然从传统业务监控到全面自动化业务 监控必将是一个持久的过程。 十一、总结 将新监控系统命名为 Horus,寓意该系统能够像古埃及神话中的 Horus 神一样,拥有敏锐的 鹰眼,及时准确地发现业务数据上的异常。Horus 系统也是我们从传统业务监控转向自动化 业务监控的一款突破性产品,针对高度复杂业务实现面向用户体验、面向业务可用性的实时 监测和自动告警,让业务监控更加简洁、自动和高效。 云计算篇 335 云计算篇 云计算篇 336 携程容器云实践 [作者简介]吴毅挺,携程系统研发部高级总监。2012 年加入携程,从零组建携程云平台团队, 目前负责携程私有云、虚拟桌面云、网站应用持续交付等研发。 一、在线旅游与弹性需求 近年来随着大众旅游消费的火热,携程的业务每年呈高速增长,2016 年 Q4 财报显示携程 2016 年全年营业收入同比增长 76%,交通票务营业收入同比增长 98%,酒店预订营业收入同 比增长 56%,其他 BU 也有大幅增长,预计 2018 年携程的 GMV 将突破 10000 亿,并在 2021 年突破 2 万亿。 我们开发的私有云和持续交付平台为携程超过 20 个 BU/SBU 服务,为了同步支撑业务的 高速发展,我们也需要不断的技术革新,持续提升携程运营、研发的效率,缩短产品从 idea 到交付用户的时间。 旅游出行的特点是季节性的,在平时流量都比较低,但节假日前一天流量会突然增高很多。 因此每到节假日就会面临成「倍」增长的扩容需求。图 1 中可以明显的看到流量的起伏情 况。 一般情况,临时扩容的需求较多,缩容会比较少,因为流量一上去之后,在短时间内不会下 来,另外一方面,哪怕是流量下来了,由于以往资源申请没那么灵活,一般都会占用资源不 释放,只能通过一些运维手段来监测资源使用情况然后告知业务部门去主动缩容。 携程目前大部分还是虚拟机,我们想缩短提前扩容的时间,以目前的虚拟机扩容方式,单个 虚拟机扩容(从分配资源、调度、网络、os 基础环境、应用部署等)至少是十分钟级别的, 云计算篇 337 如果是每次扩容上千台的话,确实需要一定的时间,而且还得看看有无足够的资源,比如 1 月 1 日的流量提前一周就扩容好,但这不够灵活,业务流量下来以后需要缩容,目前的速 度还是不够快。 针对这一场景,我们需要解决的问题是,能有更快的方案吗?如何能够做到更快速的弹性伸 缩满足业务的需求?答案是利用容器。 再举个例子,携程有个深度学习的小诗机项目,将训练好的模型,对外提供服务,用户只要 上传照片,后台的 AI 机器人就会根据照片中的内容自动写诗,这对于现行都市词穷一族来 说,瞬间提升了意境,蛮有意思的。 该项目希望在春节前上线,需要紧急扩容 1000 核,以满足春节期间大流量的需求,春节过 后立马就可以缩容 90% 的资源。目前我们通过容器可以做到 1000 核的资源,5 分钟内完 成 150 个容器实例的扩容,而且这还是 API 同步创建的速度,我们正在优化成异步的方式, 相信后续提高并发能力后,速度还可以得到大大的提升。 其实携程的容器化已经进行一年多了,容器给我们最大的感觉是看起来简单,但要做好很难, 原理不是很复杂,但是要利用这个技术做出一个产品或服务,中间有非常多的细节需要完善, 云计算篇 338 比如如何做到用户体验更好的 UI 可视化、如何做到灰度发布、容器监控、容器基础镜像版 本管理等等。 二、携程容器云定位 携程容器云定位有以下 4 点: 1、打造极致的妙级持续交付体验,服务 20+BU 秒级意味着所有的扩容、缩容、回滚全部是秒级的,做到秒级是很难的,有很多需要突破的 地方。比如,高速的镜像下发系统;高效的调度系统,稳定的容器服务,高度自动化的网络 服务。 2、提升资源利用率 为了提高服务器资源利用率,我们采取账单的形式,督促业务线提高资源利用率,优化代码 架构。我们对采集到的实时监控数据进行统计分析,按照 CPU、内存、存储、网络等多个纬 度,按月计费,每个月会将账单发给业务线的 CTO。 3、组件服务化(mysql/kv/mq/...)or PaaS 化 应用所需要依赖的很多组件能够变成服务化,AWS 或者阿里云也做了很多这种服务。携程 内部也在尝试把一些公共组件服务化,例如,MySQL,Redis,RabbitMQ 等。拿 MySQL 为 例,我们让用户可以在测试环境快速部署 MySQL 实例,并且可以选择性的将测试数据灌入。 新建的 MySQL 实例也会自动在数据访问中间件中完成注册,方便开发人员、测试人员快速 搭建测试环境和测试数据。 4、从自动化到一定程度智能化 从自动化到一定程度智能化指的是基础设施变得更智能,比如能够具备一定的自我修复能 力,如果是从上游到下游的一整套服务都具备智能化修复能力的话,这是一个非常大的突破, 对于提升运营效率和故障恢复速度至关重要; 三、容器部署基本原则 单容器单应用 单容器单 IP,可路由的 IP 容器镜像发布 immutable infrastructure 容器内部只运行 App,所有 agent 部署在 host 层面-包括监控/ES/salt 等 agent 以上是携程容器部署基本原则,看起来很容易,却是我们很长时间实践经验的总结。 比如单容器单应用这个原则,历史原因我们有混合部署的情况,单个 vm 部署多个应用,运 云计算篇 339 维的复杂度会上升很多,比如:应用之间如何做到更好的资源隔离?发布如何避免相互之间 的冲突?等等,使得我们的运维工具、发布工具变得格外复杂,开发、测试的持续交付效率 也受到极大影响; 容器镜像发布也是我们做的一个比较大的突破,过去是代码编译成可发布的包,直接部署到 vm 内部,而现在是编译时直接生成容器的镜像,不同环境其实部署的是同一个镜像,并且 不允许部署之后单独登陆进行配置修改,通过这种方式做到 immutable infrastructure ,保 证开发、测试、生产环境的一致性,同时也保障了自动化扩容、缩容快速高效的进行。 是否在容器内部也运行各种运维 agent 也是我们经过实践确定下来的;我们希望容器尽量 简单,尽可能只包含运行的应用本身,此外将所有的 agent 合并到 host 层面,也能在很大 程度上提升服务器资源利用率,agent 数量下降一到两个数量级;但配套的管理工具(eg: salt) 需要做一次升级改造才能适配新的模式; 四、容器编排选型&取舍 1、OpenStack 携程除了容器之外的东西都是用 OpenStack 来管理的,OpenStack 可以用一个模块(nova- docker)来管理容器,携程在 OpenStack 方面有多年的二次开发技术积累、也大规模的部署 运维经验,但最终没有选用 OpenStack,因为 OpenStack 整体过于复杂,调度效率也比较 低,API 调度是 10 秒以上,可以进行优化,但我们觉得优化代价太大,OpenStack 整体的 复杂度也很高; 我们早期的胖容器(把容器当 vm 来使用,做代码包发布)的确是用 OpenStack 来做的,原 因是我们希望把注意力放在容器本身,以最低的代价将容器先用起来,积累开发、运维经验; 而到了瘦容器阶段(基于容器镜像做发布),我们发现 OpenStack 整体的设计理念从本质上 讲还是为虚拟机隔离粒度的虚拟化方案设计的,而容器本身与 vm 其实差别很大,玩法也相 去甚远, 于是我们对 Mesos/K8s 进行评估; 回顾我们的容器调度探索之旅,基本上可以用以下三个阶段来总结: 第一阶段,需要最快的使用起来,用最熟悉的技术来解决部署和调度。到了第二阶段有新的 需求,引入 mesos 和 chronos,提供分布式 cron job 调度。第三阶段是使用 Python 重 新实现 chronos, 并且单独实现了 CExecutor 等组件。 云计算篇 340 OpenStack用于管理bm/vm是很合适的,并且在网络方面有很成熟的支持,无论是vlan+OVS 还是最新的 SDN 都能适配,尤其各大厂商的支持力度都很大;这也是为什么我们虽然不用 openstack 调度容器,但容器的网络其实还是用 neutron 来管理的; 2、K8S K8S 有很多很先进的设计理念,比如有 replication controller/Pod/Yaml 配置管理等,但这 些理念在携程都很难落地,因为跟现有的运维模式、发布流程有较大的冲突。而且当前还缺 乏大规模部署案例,网络尚无比较成熟的方案, 例如 L4/L7 负载均衡; 而在携程 L4/L7 服务 已经比较成熟稳定, 并且与我们现有的发布系统 Tars 集成得非常好; 3、Mesos Mesos 和 K8S 解决问题的思路是不一样的,基于 Mesos 我们可以非常容易的开发出适合 我们场景的调度框架,并且非常容易和我们现有的运维基础服务对接集成;包括 L4/L7 负 载均衡、发布系统等; 云计算篇 341 五、容器网络选型 Neutron+OVS+VLan -DPDK -https 硬件加速 -单容器单 IP,可路由 vxlan+SDN controller -vxlan offload TOR 解封装 Neutron+OVS+VLan,这个模式非常稳定,对于网络管理也是非常的透明的。这也是携程的 选择,现在的网络无论是胖容器还是容器轻量发布都用这种模式。我们也在测试 DPDK 和 https 硬件加速的效果。 我们也评估过类似 flannel 的网络,要求每个物理机独立网段,通过这个特性来做路由;非 常不灵活的一点是容器如果迁移到另外一台物理机就需要换 IP,无法满足我们的需求; 接下来会走 vxlan+ 基于 BGP EVPN 协议自研轻量级 SDN controller 的路线,vxlan offload TOR 解封装提高性能;对于 openstack 可见的还是大二层网络(vlan), 而实际上是通过 underlay 的三层路由进行转发;openstack 与我们的控制器能实现元数据的一致性;关于这 块,后续我们也会有相应的分享单独进行探讨; 如何配置容器网络 云计算篇 342 如图 5 用 dwait 和 dresponse,基于 Go 开发,dwait 会通过 unix socket 请求外部服务 dresponse 做容器的初始化。当这个容器起来的时候,它的网络没有那么独立,在 Docker 里面是需要依赖外部提供信息的,所以需要知道哪个网段或者说创建的 neutronport 再配 置到 Docker 容器内。这样做后就不会有网络丢失的情况。 六、Docker 遇到的问题 接下来分享一下我们碰到的一些比较经典的 Docker/Mesos 相关的问题 1、Docker Issue 云计算篇 343 在我们尝试使用 Chronos 跑 cronjob 时,由于我们的 Job 执行频率非常高,导致物理机 上出现非常频繁地容器创建和销毁,容器的创建和销毁比单个进程的创建和销毁代价大,会 产生很多次内核的调用,磁盘的分配销毁,这对内核是一个非常大的压力考验。 我们在实际操作过程中就遇到了一个 bug,如图 6 这台机器逐步失去响应,产生了 kernel soft lockup,慢慢的导致所有进程都死锁了,最终物理机重启了。为了避免频繁创建销毁容 器,我们没有在 Chronos 这种一个 task 一个容器的路上继续走下去,我们自己研发了 mesos framework,改成了一个 Job,一个容器的调度方式。 2、Mesos Issue  running 的容器数量较多以后,无法再启动新的容器  kernel.keys.root_maxkeys debian default 200,centos default 1M  mesos docker inspect 执行低效,尤其是单机容器数量大  MESOS_GC_DELAY:6h 20K->1h  MESOS_DOCKER_REMOVE_DELAY 1m  docker force pull false  API 性能差,功能不完善,获取异步 event 困难  overall,很稳定,调度性能足够 Mesos 性能很稳定,基本上不需要修改 Mesos 代码,只需要在我们自己写的 Framework 进行控制调度,控制如何启动容器。 3、CExecutor 云计算篇 344 1)自定义 CExecutor,Go 语言实现 -避免过于频繁创建删除容器,带来的副作用 -cpuload 高而且抖动很大 -频繁启停容器引发的 docker 和 kernel 的 bug 2)task:container -1:1->N:1 3)容器持久化 如图 7,可以观察得到前段抖动非常厉害(如果过渡频繁地创建删除容器,会带来非常大的 负担,抖动会非常高),在用 1:1 调度之后就变得平缓了。所以携程自定义 CExecutorr(Go 语言实现),避免过于频繁创建删除容器,带来的副作用(抖动非常强、load 非常高),之后 就基本上处于水平线了。 七、容器监控方案 1、Mesos 监控 云计算篇 345 如图 8-9 携程用了很多开源技术,Telegraf、influxdb、Grafana 并做了一些扩展来实现 mesos 集群的监控,采集 mesos-master 状态、task 执行数量、executor 状态等等,以便当 mesos 集群出现问题时能第一时间知道整个集群状态,进而进行修复,此外, 我们还从 mesos 调度入手,做了一些应用层的监控,比如: 针对 cron job 类型的应用,让用户可以看 到 job 应该在什么时候执行,执行的过程,整个 job 的成功率,job 还有多个实例在跑等; 2、容器监控 云计算篇 346 携程监控团队全新开发了一套监控系统 hickwall,也实现了对容器的监控支持;hickwall agent 部署在容器物理机上,通过 docker client 、cgroup 等采集容器的运行情况,包括 CPU 、 Memory、Disk IO 等常规监控项;由于容器镜像发布会非常频繁的创建、删除容器,因此我 们把容器的监控也做成自动发现,由 hickwall agent 发现到新的容器,分析容器的 label 信 息(比如: appid、版本等)来实现自动注册监控;在单个容器监控的基础上,还可以按照应用 集群来聚合显示整个集群的监控信息; 除此之外,携程还做了各个业务订单量的监控,比如说今天有多少出票量、酒店间夜数,而 我们可以非常精准地根据历史的信息预测未来的数据,比如说明天的这个时间点订单量应该 在多少、准确性在 95% 以上,一旦比预估的偏差太大的话,就会告警有异常,它把一个综 合的业务运行健康度提供给业务研发团队,而不仅仅是单个容器的运行情况。 我们在线也会做一些压测,比如说这个集群下面有 10 台机器,这 10 台机器的负载均衡权 重都是 0.1,我们会把其中一台调高,看它的吞吐和响应的情况。测出它到了一定极限能提 供的 QPS,就可以知道这个集群还剩多少性能高容量,这个容量就是这个集群还能承载多大 的压力,比如说有 25% 的富余,根据订单的变化就可以知道还多多少或者还缺多少,这样 就能做到更好的扩缩容调度;目前基于 vm 的应用已经能基于容量规划和预测实现自动扩 容,后续容器的扩缩容也会接入,并且做到更实时的扩容和缩容调度; 此外,容器监控对于携程创新工场的团队是很有意义的,这些新孵化的 BU 对成本控制更严 格,随着容器上线,我们能为其提供性价比更高的基础设施。 八、CDOS Overview 云计算篇 347 CDOS 全称是 CtripData Center Operating System, 我们希望通过 CDOS 来调度多个数据中 心的资源,就好比一个操作系统调度各种资源给各个进程一样,CDOS 会调度多个数据中心 的计算、网络、存储的资源给到不同的应用,满足各个应用所需的冗余度,并且会动态的维 持这个冗余度,一旦出现异常,可以自动尝试修复,删除出现问题的容器实例,并部署新的 实例;这里面会涉及到非常多的模块。 如图 11 最上层是持续交付的发布系统,这一层是跟应用交付相关的东西,开发人员、测试 人员都会用的发布系统。下面是针对不同运行模式的应用定制的两套调度管理模块,一个是 cron Job,另一个是 long running service;两者在管理、部署、发布方面都有一些差异化; 底层资源分配是用 Mesos 来实现,历史原因,我们还有大量的服务部署在 windows 上, 因 而需要同时支持 windows server container 和 docker。长期来看未必是继续使用 Docker, 因为 Docker 太激进了,目前已经有多种 container 实现方式可以选择。Windows 容器方面 携程也已经做了一些 POC,实现类似前面提到的 ovs + vlan 的网络接入,遵循单容器单 ip 等原则,并且投入覆盖了部分的测试环境。 图中右边有很多服务,比如 L4/L7 负载均衡、 SOA 的服务,CMS 应用元数据管理、监控、 日志收集、镜像管理和推送等模块;由于我们采用容器镜像发布,要实现秒级交付,对于镜 像推送速度有很高的要求,当 base image 没有变化时,只需要把应用新版本 build 出来的 app 层通过我们开发的 Ceph 同步模块推送的多个数据中心;当 base image 也变更时,情 况会更复杂,除了同步到各个数据中心的 Ceph 对象存储,还需要预先下发到可能用到的 Docker 服务器,才能以最快的方式启动容器。 云计算篇 348 以上就是携程容器云的概况介绍,欢迎大家一起来交流和探讨。 云计算篇 349 携程容器云优化实践 [作者简介]王潇俊,多年来致力于云平台及持续交付的实践。2015 年加入携程,参与携程部 署架构的全面改造,主导设计和打造新一代的适用于微服务的发布系统。同时负责基于携程 私有云的兼容虚机与容器的持续交付平台。ROR 狂热粉丝,敏捷文化的忠实拥趸。 随着微服务架构的流行,把容器技术推到了一个至高点上;而随着 Docker,K8S 等容器技术 的日趋成熟,DevOps 的概念也再次热度上升;面对容器化的大潮趋势,各家公司都在积极 地响应和实践,携程也在这方面做了不少工作,形成了自己的容器云平台。 从容器云的打造思路上,携程将其划分成了水上、水下两大部分: 水下部分是指容器云服务的基础架构 水上部分是指面向容器而产生的一系列工程实践配套 水下部分对 Dev 来说相对透明,而水上部分则会对 Dev 工作有直接影响,也就是 DevOps 概念里所提到“混乱之墙”所在的地方。所以只要水上、水下同时做好了,容器云才能真正 落地,并符合 DevOps 理念的设想。 一、基础架构 容器云在携程主要经历了以下 3 个阶段: 第一阶段,模拟虚机,通过 OpenStack 进行管理 在这个阶段,主要的目的是验证携程已有的应用是否能够在 Docker 容器下正常运行,并提 云计算篇 350 升系统与容器的兼容性。这个过程中系统的主要架构还是使用 OpenStack 的 nova 模块,把 Docker 模拟成虚机的形式进行管理,除了应用实际运行的环境产生了变化,其他任何流程, 工具都不变,从而使影响范围控制在最小。 第二阶段,实现镜像发布,使用 Chronos 运行 Job 应用 在这个阶段,主要的目的是通过镜像的方式实现应用的发布和变更。真正实现 Immutable Delivery,即一旦部署后,不再对容器进行变化。并且在这个过程中,架构从比较繁重的 OpenStack 体系中解脱出来。使用轻量级的 Mesos+Chronos 来调度 Job 应用,在这个过程 中我们同时去掉了对 Long Running 的 Service 类型应用的支持,以方便测试在极端情况下 调度的消耗,和整个系统的稳定性。 此时,整个容器云的架构如下图, 实践证明这个架构在应对大量并发 job 的调度时,Mesos 自身调度消耗过大,因为每启动一 次 job 都需要拉起一个 docker 实例,开销客观。同时也证明在携程这样的应用体量下,直 接使用开源 Framework 是无法满足我们的需求的,这也促使我们开始走向自研 Framework 的方向。 第三阶段,自研 Framework 云计算篇 351 在这个阶段,我们主要要解决的问题有: 同时支持 Job 与 Service 两种类型的应用 为每个 docker 实例分配独立的 IP 支持 stateful 的应用 完善容器的监控体系 此时的总体架构如下图: 与第二阶段的架构不同之处: 首先,重新封装了 Mesos 的 Rest API 层,使得对外提供的 API 更丰富(可以与其他已有系 统结合,提供更多的功能),同时基于一些规范统一的考虑,收拢了一些个性化参数的使用。 除此之外,独立抽象 API 层也是为了将来能够快速适配其他架构体系,如 K8S 时,可以做到 对上应用透明。 其次,对 Mesos 做了集群化分布,从而提高 Mesos 本身的可用性。 最后,为了应对大量 Job 类应用的调度,采用了与 long running 一样的方式,将 executor 放 置于容器内部。做到 Job 调度时,不重新启动容器,而是在容器内部调度一个进程。 说完了系统架构后,还有 2 个比较重要的问题: 网络 云计算篇 352 携程对容器实例的要求是,单容器单 IP,且可路由,所以网络选项上采用的仍旧是 Neutron+OVS+VLan 这个模式,这个模式比较稳定,网络管理也比较透明。在实际对每个容 器配置网络的过程中,携程自研了一套初始化 hook 机制,以通过该机制在容器启动后从外 部获取对应的网络信息,如网段,或者 Neutronport 等,在配置到容器内,这样就完成了网 络配置的持久化。大致的机制如下图所示: 当然利用这个 hook 机制还能处理其他一些特殊的 case,之后也会有提到。 监控 云计算篇 353 监控分为 2 个部分,一块是对 Mesos 集群的监控。携程用了很多开源技术,如:Telegraf、 influxdb、Grafana 等,并做了一些扩展来实现 mesos 集群的监控,采集 mesos-master 状 态、task 执行数量、executor 状态等等,以便当 mesos 集群出现问题时能第一时间知道整 个集群状态,进而进行修复。 另一块是对容器实例的监控,携程监控团队开发了一套监控系统 hickwall, 实现了对容器的 监控支持。 hickwall agent 部署在容器物理机上,通过 Docker client 、cgroup 等采集容器 的运行情况,包括 CPU 、Memory、Disk IO 等常规监控项;由于容器镜像发布会非常频繁 的创建、删除容器,因此我们把容器的监控也做成自动发现,由 hickwall agent 发现到新的 容器,分析容器的 label 信息(比如: appid、版本等)来实现自动注册监控;在单个容器监控 的基础上,还可以按照应用集群来聚合显示整个集群的监控信息; 云计算篇 354 自研 Framework 的动机 轻量化,专注需求 开源 Framework 为了普适性,和扩展性考虑,相对都比较重,而携程实际的使用场景,并 不是特别复杂,只需要做好最基础的调度即可。因此自研的话更可以专注业务本身的需求, 也可以更轻量化。 兼容性,适配原有中间件 由于携程已经形成了比较完整的应用架构体系,以及经过多年打造已经成熟的中间件系列。 所以自研 Framework 可以很好地去适配原有的这些资源,使用开源项目反而适配改造的成 本会比较大,比如路由系统,监控系统,服务治理系统等等。 程序员的天性,改不如重写 最后一点就比较实在了,开源项目使用的语言,框架比较分散,长远来说维护成本比较大 自研 Framework 的甜头 正如前面所说,自研 Framework 能够很方便地解决一些实际问题,下面就举一个我们碰到 的实际例子。 我们知道 mesos 本身调度资源的方式是以 offer 的模式来处理的,简单来说就是 mesos 将 剩余资源的总和以 offer 的形式发送出来,如果有需求则占用,没有需求则回收,待下次发 送 offer。但是如果碰到下图这样的情况,即 mesos 一直给出 2 核的资源,并且每次都被占 用,那一个需要 4 核的实例什么时候能拿到资源呢? 云计算篇 355 我们把这种情况叫做 offer 碎片,也就是一个先到的大资源申请,可能一直无法得到合适的 offer 的情况。 解决这个问题的办法其实很简单,无非 2 种: 1、将短时间内的 offer 进行合并,再看资源申请的情况 云计算篇 356 2、缩短 mesosoffer 的 timeout 时间,使其强制回收合并资源,再次 offer 携程目前采用的方案 2,实现非常简单。 以上大致介绍了一下携程容器云的水下部分,即基础架构的情况,以及自研 Framework 带 来的一些好处。关于 k8s,由于我们封装了容器云对外的 API 层,所以其实对于底层架构到 底用什么,已经可以很好的掌控,我们也在逐步尝试将一些 stateful 的应用跑在 k8s 上,做 到 2 套架构的并存,充分发挥各自的优势。 云计算篇 357 二、工程实践 容器化的过程除了架构体系的升级,对原先的工程实践会带来比较大的冲击。也会遇到许多 理念与现实相冲突的地方,下面分别介绍携程遇到的一些实际问题和解决思路。 代码包到镜像,交付流程如何适配,如何迁移过渡? DevOps 理念提倡“谁开发,谁运行”,借助 docker 正好很方便的落地了这个概念。携程的 CI/CD 系统同时支持了基于镜像与代码包的发布。这样做的好处是能够在容器化迁移的过程 中做到无缝和灰度。 能像虚机一样登陆机器吗?SSH? docker 本身提倡单容器单进程,所以是否需要 sshd 是个很尴尬的问题。但是对于 docker 实 例的控制,以及执行一些必须的命令还是很有必要的,至少对于 ops 而言是一种非常有效的 排障手段。所以,携程采用的方式是,通过 web console 与宿主机建立连接,然后通过 exec 的方式进入容器。 云计算篇 358 Tomcat 能否作为容器的主进程? 我们知道主进程挂掉,则容器实例也会被销毁。而 Java 开发都知道,tomcat 启动失败是很 正常的 case。由此就产生了一个矛盾,tomcat 启动失败,并不等同于容器实例启动失败, 我们需要去追查 tomcat 启动失败的原因。由此可见,tomcat 不能作为容器的主进程。因此, 携程仍旧使用 Supervisord 来维护 tomcat 进程。同时在启动时会注册一些自定义 hook,以 应对一些特殊的应用场景。比如:某些应用需要在 tomcat 成功启动,或成功停止后进行一 些额外的操作,等等。 JVM 配置是谁的锅? 容器上线后一段时间,团队一直被一个 JVM OOM 的问题所困扰,原来在虚机跑的好好的应 用,为什么到容器就 OOM 了呢?最后定位到问题的原因是,容器采用了 cpu quota 的模式, 但 JVM 无法准确的获取到 cpu 的数量,只能获取到宿主机 cpu 的数量;同时由于一些 java 组件会根据 cpu 的数量来开启 thread 数量,这样就造成了堆外内存殆尽,最终造成 OOM。 虽然,找到了 OOM 的原因,但是对于容器云来说,却面临了一个棘手的问题。容器实例不 像虚机,在虚机上,用户可以按需定义 JVM 配置,然后再将代码进行发布。在容器云上, 发布的是镜像,JVM 的配置则变成了镜像的包含物,无法在 runtime 时进行灵活修改。 云计算篇 359 而且,容器本身并不考虑研发流程上的一些问题。比如,我们有不同的测试环境,不同的测 试环境可能有不同的 JVM 配置,这显然与 docker 设想的,一个镜像走天下的想法矛盾了。 最后,对于终端用户而言,在选择容器时,往往挑选的是 flavor,因此我们需要对应不同的 flavor 定义一套标准的 JVM 配置,利用之前提到的容器启动时的 hook 机制,从外部获取该 容器匹配的标准 JVM 配置。 我们也总结了一些对于对外内存的最佳实践,如下: • Xmx = Xms = Flavor * 80% • Xss = 256K • 堆外最小 800,最大 2G,符合这个规则之内,以 20%计 问题又来了,用户需要自定义 JVM? 最终,我们将 JVM 配置划分成了 3 个部分: 1、系统默认推荐部分 2、用户自定义 override 部分 3、系统强制覆盖部分 允许用户通过代码或外部配置系统,对应用的 JVM 参数进行配置,这些配置会覆盖掉系统 默认推荐的配置,但是有一些配置是公司标准,不允许覆盖的,比如统一的 jmx 服务地址 等,这些内容则会在最终被按标准替换成公司统一的值。 云计算篇 360 Dockerfile 的原罪 Dockerfile 有很多好处,但同时也存在很多坏处: 无法执行条件运算 不支持继承 维护难度大 可能成为一个后面,破坏环境标准 因此,如果允许 PD 对每个应用都自定义 dockerfile 的话,很有可能破坏已有的很多标准, 产生各种各样的个性化行为,使得统一运维变成不可能,这种情况在携程这样的运维体谅下, 是无法接受的。 打造“plugin”服务平台 所以,携程决定通过 “plugin”服务的方式,把 dockerfile 的使用管控起来,将一些常规的 通过 dockerfile 实现的功能形成为“plugin”,在 Image build 的过程中进行执行。这样做的 好处是,所提供的服务可标准化,并且可复用,还可以任意组装。比如:我们分别提供“安 装 FTP”,“安装 Jacoco”等插件服务。用户在完成自己的代码后,进行 image build 时就 云计算篇 361 可以单选或多选这些服务,那最后形成的 image 中就会附带这些插件。并且针对不同的测 试环境可以选择不同的插件,形成不同的镜像。 对于一个“plugin”而言,甚至可以定义一些 hook(注册 supervisord hook),以及一些可 exec 执行的脚本,从而进一步扩展了“plugin”的能力。比如可以插入一个 tomcat 的启停 脚本,从而获取从外部控制容器内 tomcat 的能力。 云计算篇 362 公司内的每个 PD 都可以申请注册“plugin”,审核通过后,就可以在平台上被其他应用所 使用。注册步骤: 1、为服务定义名称和说明 2、选择服务可支持的环境(如:测试,生产) 3、上传自定义的 dockerfile 4、上传自定义的可运行脚本 云计算篇 363 “Jacoco Plugin”的实例 Jacoco 是一个在服务端收集代码覆盖率的工具,以帮助测试人员确认测试覆盖率。这个工 具的使用有以下几个需求: 1、需要在代码允许环境中安装 Jacoco agent 2、只需要在特定的测试环境进行安装,生产环境不能安装 3、被测应用启动后,需要往 Jacoco 后端服务进行注册 4、测试过程中可以方便控制 Jacoco 的启停(通过 tomcat 启动参数控制) 针对以上的需求,定制一个“JacocoPlugin”的工作,如下图: 1、通过 dockerfile 安装 jacoco agent 云计算篇 364 2、注册一个 supervisord hook,在 tomcat 启动成功后向 Jacoco service 进行注册 3、利用一个自定义 tomcat 重启脚本,并在平台的 web server 上暴露 api 来控制 jacoco 的 启停 这样,所有容器云上的应用在 image build 时就都可以按需选择是否需要开通 jacoco 服务 了。 利用这样的平台机制,还提供了一系列其他类型的“plugin”服务,以解决环境个性化配置 的问题。 三、总结 1、devops 或者容器化是理念的变化,更需要接地气的实施方案 2、基础架构,工程实践和配套服务,需要并进,才能落地 3、适合自己的方案才是最好的方案 携程的容器云进程还在不断的进化之中,很多新鲜的事务和问题等待着我们去发现和探索。 数据库篇 365 数据库篇 数据库篇 366 MySQL 时间序列存储引擎的设计与实现 [作者简介]姜宇翔,携程技术保障中心高级研究员。具有十多年数据库领域开发经验,曾参 与国产自主品牌达梦数据库版本 4 到版本 7 开发的全过程,具有丰富的数据库知识和开发 经验。加入携程后,主攻 MySQL 的源码研究和改造,在 MySQL 的存储引擎/主从复制/审计 等领域皆有贡献,并对各种特性需求,进行针对性的功能开发。 当我们使用 MySQL 的时候,经常感慨多引擎下数据管理的灵活。不论是 innobase 这样带有 ACID 特性的数据引擎后端,还是 black hole 这样吃掉数据什么也不做的数据引擎后端,都 在不同场合发挥着自己的作用。 享受着各种存储引擎带来的便利时,我们也注意到 MySQL 的存储引擎开发在国内还是一片 蓝海。毕竟这种以插件方式加载到 MySQL 的数据后端,是一个较深涉及到底层的领域,数 据库底层开发在国内也只有一少部分开发者走在这条路上。这不能不说是一个遗憾的事情。 携程技术保障中心的 MySQL 时间序列存储引擎缘起于一次讨论,讨论的议题是关于哪个时 间序列数据库更适合携程环境。在这次日常的自由讨论中,有人突发奇想的提出,我们是否 可以开发一个 MySQL 的时间序列存储引擎?没有已有的时间序列数据部署的繁琐,没有那 些各具特色的接口调用,不需要熟悉新的系统,还是以 SQL 的方式来进行数据的访问。 首先,让我们看看 OpenTSDB 是什么样的情况。下图便是 OpenTSDB 的部署与运行图示。 OpenTSDB的后端存储是 HBASE,需要在各个server 上部署信息收集的前端,通过dashboard 展示信息。对于现有的时间序列数据库,每一个都有自己的部署与运行方案。而这些方案并 不具备架构上的通用性。 数据库篇 367 我们所期望的架构是除了底层的存储组件不同,对于 MySQL 的用户来说没有什么不同。已 有的大部分运维经验(HA、复制、备份等等)和已有的开发经验(插入、删除和更新的操 作)都可以继承自之前的积累,这是任何一个使用 MySQL 的公司所希望的情况。就如下图 所展示的架构,对于上层用户来说,感觉不到太多的变化。用户可以通过标准的 SQL 编写 自己的应用客户端来完成数据的采集和展示,提高灵活性。 经过以上的考量,产生了我们的试验产品,存储引擎 CFL(ctrip fast log),该引擎能够以快 速的日志方式进行数据的记录。其完成后就如下图所示,满足之前所设想的种种情况。 一、技术介绍 从层次结构来看,MySQL 的存储引擎分为两个部分。一是实现存储功能相关的组件,该层 提供存储引擎的具体功能(增删改查),我们称之为功能层;一是和 MySQL 插件接口对接的 组件,该层将 MySQL 的功能调用转换为存储引擎的功能调用,我们称之为接口层。如下图 所示,数据库的操作(诸如增删改查等操作)将通过引擎管理层达到存储引擎的接口层,再 由接口层到达功能层。 数据库篇 368 功能层 功能层是存储引擎的核心,由于设计目标的不同,存储引擎的功能层所提供的功能也是不同 的。比如 innobase 引擎的功能层,提供了事务 ACID/数据存储/元信息管理/MVCC 等一系列 功能,提供完整的数据库功能;再如 CSV 存储引擎,仅提供字符类型的行存储。这些不同 功能的存储引擎组件,在 MySQL 的框架下,提供多种多样的服务。 介绍携程时间序列存储引擎的功能层将从两个主要方面介绍。一是功能层的架构,也就是运 行时涉及到的对象和这些对象的作用;一是进行持久化的存储,该部分将说明在文件层面, 数据是如何存储的。 架构 CFL 的架构设计目标是尽可能的提高数据的插入效率,因此并行处理的想法需要贯穿始终。 其机制为,不同会话并行的将数据向表对象中插入;表对象通过缓冲区保存插入的数据,当 缓冲区写满之后,缓冲区加入磁盘写入队列,通过专门的写盘线程并发的写入磁盘。 下图为携程时间序列存储引擎的架构: 数据库篇 369 存储 在设计存储的时候,根据时序数据库的特点,首先考虑的是插入的效率,然后是快速的故障 恢复。针对插入效率,在设计数据结构时,采用严格的顺序写入策略,以此来保证连续插入 的效率。这样不论在传统硬盘还是在 SSD 硬盘上,都可以高效的写入。针对快速故障恢复, 通过控制写入顺序(依次写入数据、索引和控制信息),实现快速的恢复。最后考虑实现上 的简易性,采用索引和数据分别存储的方式,降低在同一文件中进行存储管理的控制。 接口层 程序片段 如下代码片段为接口层部分。 数据库篇 370 MySQL 提供的基类 handler , 存 储 引 擎 需 要 提 供 继承自该类,并实现基类中如 ha_open/ha_close 等功能函数的类。 继承 handler 类的携程时间序列数据库的类。 接口层架构 该部分将以 ha_cfl 为例,说明 MySQL 存储引擎管理层和引擎接口之间的关系。 数据库篇 371 操作发送给会话后,会话从引擎管理层获取到 ha_cfl 的对象,将操作转化为对 ha_cfl 接口的 调用,该步骤完成了 SQL 到存储引擎接口的对接。 ha_cfl 接口接到调用后,将调用转化为对表对象的操作,完成 handler 接口功能到表对象的 实现的对接。 二、效果 通过对时间序列数据进行针对性的开发,CFL 存储引擎的插入性能相对于 InnoDB 和 MyISAM 引擎有很大的提高。 数据库篇 372 ips:insert per second 三、总结 由于本次开发为探索性质的开发,时间上或者人力上的限制使产品还不够完善,不论是设计 还是实现上都存在需要改进的地方。如,创建表时对时间戳类型使用和索引列的指定存在限 制,导致无法创建多列索引,仅能够创建时间索引。存储结构的限制导致删除和更新无法快 速灵活的进行。 但在资源有限的情况下,完成一个概念完整和实现完整的产品。而且正是通过这次探索性开 发,打开了 MySQL 存储引擎的一扇大门,不论从整体架构到实现细节都有深入研究,积累 了很多经验。携程技术保障中心 DBA 团队希望这些经验在将来能够为国内的 MySQL 社区提 供帮助。 数据库篇 373 一个 MySQL 5.7 分区表性能下降的案例分析 [作者简介]姜宇祥,2012 年加入携程,10 年数据库核心代码开发经验,相关开发涉及达梦, MySQL 数据库。现致力于携程 MySQL 的底层研发,为特殊问题定位和处理提供技术支持。 前言:希望通过本文,使 MySQL5.7.18 的使用者知晓分区表使用中存在的陷阱,避免在该版 本上继续踩坑。同时通过对源码的分享,升级 MySQL5.7.18 时分区表性能下降的根本原因, 向 MySQL 源码爱好者展示分区表实现中锁的运用。 一、问题描述 MySQL 5.7 版本中,性能相关的改进非常多。包括临时表相关的性能改进,连接建立速度的 优化和复制分发相关的性能改进等等。基本上不需要做配置修改,只需要升级到 5.7 版本, 就能带来不少性能的提升。 我们在测试环境,把数据库升级到 5.7.18 版本,验证 MySQL 5.7.18 版本是否符合我们的预 期。观察运行了一段时间,有开发反馈,数据库的性能比之前的 5.6.21 版本有下降。主要的 表现特征是遇到比较多的锁超时情况。开发另外反馈,性能下降相关的表都是分区表。更新 走的都是主键。这个反馈引起了我们重视。我们做了如下尝试: 数据库的版本为 5.7.18, 保留分区表,性能会下降。 数据库版本为 5.7.18,把表调整为非分区表,性能正常。 把数据库的版本回退到 5.6.21 版本,保留分区表,性能也是正常 通过上述测试,我们大致判定,这个性能下降和 MySQL5.7 版本升级有关。 二、问题重现 测试环境的数据库表结构比较多,并且调用关系也比较复杂。为了进一步分析并定位问题, 我们抽丝剥茧,构建了如下一个简单的重现过程 // 创建一个测试分区表 t2: CREATE TABLE `t2`( `id` INT(11) NOT NULL, `dt` DATETIME NOT NULL, `data` VARCHAR(10) DEFAULT NULL, PRIMARYKEY (`id`,`dt`), KEY`idx_dt`(`dt`) ) ENGINE=INNODB DEFAULTCHARSET=latin1 /*!50100 PARTITION BY RANGE (to_days(dt)) (PARTITION p20170218 VALUES LESS THAN (736744)ENGINE = InnoDB, PARTITIONp20170219 VALUES LESS THAN (736745) ENGINE = InnoDB, 数据库篇 374 PARTITIONpMax VALUES LESS THAN MAXVALUE ENGINE = InnoDB) */ // 插入测试数据 INSERT INTO t2 VALUES (1, NOW(), '1'); INSERT INTO t2 VALUES (2, NOW(), '2'); INSERT INTO t2 VALUES (3, NOW(), '3'); // SESSION 1 对 id = 1 的 记录 做一个更新操作,事务先不提交。 BEGIN;UPDATE t2 SET DATA = '12' WHERE id = 1; // SESSION 2 对 id = 2 的记录做一个更新。 BEGIN;UPDATE t2 SET DATA = '21' WHERE id = 2; 在 SESSION 2,我们发现,这个更新操作一直在等待。ID 是主键,按道理,主键 id = 1 的 记录更新,不至于影响到主键 id = 2 的记录更新。 查询 information_schema 下的 innodb_locks 这张表。这张表是用于记录 InnoDB 事务尝试申 请但还未获取的锁,以及阻塞其他事务的事务所拥有的锁。有两条记录: 观察此时的 innodb_locks 表,事务 id=40021 锁住第 3 页的第 2 行记录,导致事务 id=40022 无法进行下去。 我们把数据库回退到 5.6.21 版本,则不能重现上述场景。 三、进一步分析 根据 innodb_locks 表提供的信息,我们知道问题在于 InnoDB 锁定了不恰当的行。该表是 memory 存储引擎。我们在 memory 存储引擎的插入接口设置断点,得到如下堆栈信息。确 定是红框部分,将锁信息写入到 innodb_locks 表中。 数据库篇 375 并在函数 fill_innodb_locks_from_cache 中得以确认,每次写入行的数据,都是从如下代码中 Cache 对象中获取的。 我们知道 Cache 中保存了事务锁的信息,因此需要进一步查找 Cache 中的数据,是如何添 加进去的。通过搜索 cache 对象在 innodb 代码中出现的位置,找到函数 add_lock_to_cache。 在此函数设置断点进行调试后,发现其内容与填写 innodb_locks 表的数据一致。确定该函数 使用的 lock 对象,就是我们要找的锁对象。 数据库篇 376 针对 lock_t 类型的使用位置进行排查。经过筛选和调试,发现函数 RecLock::lock_add 中, 生成的行锁被加入到该锁所在的事务链表中。 RecLock::lock_add 函数可以推出行锁的生成原因。因此,通过对该函数进行断点设置,查看 函数堆栈,在如下堆栈内,定位到红框位置的函数: 数据库篇 377 针对 Partition_helper::handle_ordered_index_scan 的如下代码进行跟踪,根据该段代码的分 析,m_part_spec.end_part 决定了进行上锁的最大行数,此处即为非正常行锁生成的原因。 最终问题归结到 m_part_spec.end_part 的生成原因。通过对 end_part 使用地方进行排查, 最终在 get_partition_set 函数中定位到该变量在使用前的初始设置值。从代码中可以看出, 每次单条记录的 update 操作,在进行 index scan 上锁时,对分区表数目相同的行数进行上 锁。这个是根本原因。 数据库篇 378 四、验证结论 根据之前的分析,每次单条记录的 update 操作,会对分区表数目相同的行数进行上锁。我 们尝试验证我们的发现。 新增如下两条记录: INSERT INTO t2 VALUES (4, NOW(), '4'); INSERT INTO t2 VALUES (5, NOW(), '5'); // SESSION 1 对 id = 1 的 记录 做一个更新操作,事务先不提交。 BEGIN;UPDATE t2 SET DATA = '12' WHERE id = 1; // SESSION 2 现在对 id = 4 的记录做一个更新。 BEGIN;UPDATE t2 SET DATA = '44' WHERE id = 4; 我们发现,对 id = 4 的更新可以正常进行。不会受到 id = 1 的更新影响。这是因为 id=4 的 记录,超过了测试案例的分区个数,不会被锁住。在实际应用中,分区表所定义分区数不会 如测试用例中的只有 3 个,而是数十个乃至数百个。这样进行上锁的结果,将加剧更新情况 下的锁冲突,导致事务处于锁等待状态。如下图所示,每个事务都上 N 个行锁,那么这些上 锁记录互相覆盖的可能性就极大的提高,也就导致并发下降,效率降低。 数据库篇 379 五、结论 通过上述分析,我们非常确认,这个应该是 MySQL 5.7 版本的一个 regression。我们提交了 一个 Bug 到开源社区。Oracle 确认是一个问题,需进一步分析调查这个 Bug。 云服务篇 380 云服务篇 云服务篇 381 揭秘携程基于融合通讯技术的新一代客服系统 [作者简介]本文作者为携程基础业务研发部呼叫中心团队,其在传统呼叫中心基础上,结合 软交换、智能分配、自动语音语义处理等技术,为携程用户提供人性化、人机互动、便捷的 电话语音服务。 一、背景 随着中国经济的发展,在线旅游服务商和传统的旅行社服务商面向不同年龄层次的客户群体 竞争,越来越多的人选择携程旅行,享受更快捷更优质的服务体验。而在旅行的过程中,尤 其是国外游、自助游比率日益增大的情况下,旅行途中遇到突发状况时,往往需要随时随地、 便捷高效的联系客服,快速解决问题。 庞大的客户群体激发的需求,也让携程基础业务呼叫中心团队有了更多的思考:如何将互联 网的技术优势延伸融合到传统客服服务中,更好的支持携程客服,让用户能享受到方便、快 捷的在线服务。 二、客服系统现状 目前各大互联网企业的客服系统,沟通模式可分为两大类:一类以电话实时语音通信为主, 例如携程、中国电信的呼叫中心,以电话为切入,用户和客服实时通话,沟通一对一,用户 问题能及时得到反馈;另一类以即时通讯为主,例如淘宝、京东等支持文本、图片和语音消 息。前者能为用户提供实时的支持、后者能提供更为丰富的沟通方式,两者各有优缺点。 某些场景下,现有的两种模式都无法满足用户的需求。  如果用户在海外遇到突发情况要联系客服,而此时无法拨打普通电话,纯在线聊天工具 即使便捷,但是不够及时,无法缓解用户焦虑的状况。此时,则需要纯网络电话来提供 及时的服务,快速解决问题  纯电话场景:用户拨打电话描述问题的时候,有时需要提供一些凭证,比如产品截图、 位置等,此时用户只能挂断电话后,再从某些入口重新提供,客服人员再处理,这个过 程会大大增加了处理的环节,降低了效率  同一个用户针对同一个事情通过在线客服和电话方式咨询客服,来回多次需要重复描述 问题,客人体验会很差。 基于以上的一些场景,携程 IM+客服系统应运而生,将实时通讯和即时技术融合一体,打造 全媒体多渠道客服服务平台,提升携程服务质量,给用户以最佳的服务体验。 三、携程 IM+客服系统简介 1、平台优势 云服务篇 382 IM+平台主要优势:  打通电话与在线聊天的模式,同一个用户咨询,可以保证分配到最优的客服(上次咨询 过),避免用户多次重复问题描述,提升体验  详情完整的历史服务轨迹,保证客服第一时间快速了解用户诉求,帮助其快速解决问题;  用户可以通过纯电话、免费网络电话、在线聊天三种方式任意切换,多渠道进行沟通, 提升沟通效率和用户体验。  当客服需要联系客人的时候, 尤其在海外用户手机不可用的情况下,IM+可以提供 VOIP 外呼功能, 帮助客服触达用户,解决紧急的问题(比如机票航变通知等) IM+系统基于呼叫中心及 IM 技术,将实时通讯与即时通讯融合到一起,同时支持传统电话、 网络电话、图片、文字、语音短信息和图像,用户与客服根据需要在这不同的通讯方式中进 行无缝切换,满足沟通中的不同需求。 基于上图的设计架构,客人可以使用传统电话拨打携程客服电话,或使用手机客户端使用网 络电话、即时消息(文字、语音短消息、图片、位置等)接入携程呼叫中心,经过智能分配 系统,将客人的服务请求分配到最优的座席服务人员。 云服务篇 383 手机客户端提供传统电话、网络电话和即时消息的交互界面,客人可以根据自己的需求选择 合适的沟通方式。 云服务篇 384 为适应客服日常经常使用的功能,客服座席应用中提供的功能信息更为丰富,帮助客服第一 时间获取用户的全面信息,以便针对性的提供优质的服务方式。除包含即时消息的同时还具 备电话控制能力,可以支持接听、外呼、转接等功能。 2、实现难点 客服系统实现过程中都会存在以下难点:  客服座席应用的发布与更新。  电话功能与业务功能的耦合。  如何将无限的用户与有限的客服资源进行及时匹配。  如何快速支持多种多样的分配规则。  如何将实时通讯与即时通讯两种不同的通信方式无缝的整合到一起。 IM+系统通过以下几个方面,解决了这些问题。 1)客服座席的应用架构 云服务篇 385 一个应用的交互界面与相关的业务会更为紧密的结合,交互形式的变化相对也更为频繁。对 于携程拥有 2 万+坐席,每次客户端的更新发布都将是一个巨大的挑战。与此同时,作为一 个与电话紧密相关的应用,需要实现与电话交换机 TCP&UDP 的通讯, Web 技术又无法做 到这点 。 结合 Hybrid 技术架构,IM+客服座席应用采用了原生程序框架内嵌 Webkit 容器的 Hybrid 架 构。  原生程序框架(Windows 或 Linux 应用)负责与操作系统及电话交换机通讯,Webkit 容 器负责用户 UI 端的展示。  原生应用框架与 Webkit 容器还能相互通信,实现 Web 页面对电话业务的控制。  原生应用框架只需要关注底层的通信功能,实现功能的高度聚合,具体的业务功能由 Web 站点提供,与 Native 框架实现解耦。 2)分布式的智能分配系统 云服务篇 386 传统的客服分配服务由于需要保证数据一致性,大部分采用有状态服务,而有状态的服务一 般都是以主备的方式部署,无法做到负载均衡。但是我们的目标是携程上亿客户群,没有一 台机器是能扛住这样的用户量。 通过对客服业务分析,我们发现客服业务有个显著特点,就是客户请求的数量是不固定的, 并且可能远大于客服数量,而客服数量是固定的并且相对较少。针对这个特点,我们将分配 服务拆分成 2 块:分配服务与客服状态服务。  分配服务是典型的无状态服务,采用集群部署,负责处理用户的分配请求与执行分配策 略。这样就不用担心分配请求量大了。  客服状态服务则负责与管理客服座席应用保持长连接,管理其状态,并向其推送消息。 当有分配请求过来时,分配服务会执行分配策略,并通知客服状态服务。 3)通用策略模型与规则引擎 在如何分配的问题上,可谓是千人千面,每个 BU 都有对应自己业务逻辑的要求。为了应对 变化多样的业务分配需求,我们将分配模型抽象出 4 个维度,分别是条件、行为、目标、顺 序。并以这四种维度,组成脚本化的分配策略。这里我们使用到了开源的 Drools 规则引擎, 它使得我们的分配策略可动态修改并即时生效而不需要调整服务代码。 云服务篇 387 4)通讯方式的整合统一 在 IM+的业务场景中,客户发起服务请求的入口可分为两大类。一类是即时消息发起,另外 一种则是从电话发起。如何将这两种类型的通讯方式整合到一起,一直是困扰我们的难题。 最终 IM+系统采用了会话的概念来统一管理,不论服务请求的发起入口即时通讯还是电话 的实时通讯都视为一个会话。相同的用户 ID 和服务类型在客服座席应用只会显示一个会话。 用这样一个概念将这两种不同的通讯方式实现统一管理,从此沟通无障碍。 云服务篇 388 携程呼叫中心移动坐席解决方案 [作者简介]本文作者为携程基础业务研发部呼叫中心团队,其在传统呼叫中心基础上,结合 软交换、智能分配、自动语音语义处理等技术,为携程用户提供人性化、人机互动、便捷的 电话语音服务。 一、前言 智能手机早已成为日常生活中不可或缺的一部分,随着移动互联网的快速发展,人们的生活 习惯与工作方式也在不断发生改变。从移动通信、移动支付,再到移动办公,“移动化”已 渗透至各行各业,并逐步成为企业业务发展的趋势。 携程呼叫中心研发团队根据业务的需求,研发完成了一套完整的呼叫中心移动坐席解决方 案,使业务坐席不再受制于工作时间、办公地点,随时随地,有网络的地方,就有呼叫中心。 二、移动办公呼叫中心系统架构 移动场景保留了典型的呼叫中心系统架构,在接入端加入 SBC,用于移动呼叫中心语音接入 和安全控制。坐席无论身在何处,只需要一台电脑、智能手机或智能设备,通过 Wifi、3G 或 4G 网络登录坐席 App,即可开启日常工作。 三、移动场景下面临的挑战 目前国内企业的Intranet基本为100M,而自有机房的核心网络可达到1000M甚至双1000M。 因为物理网络有着高度可靠的带宽、网络质量与稳定性,用户基本无需顾虑带宽、延时、网 络抖动等情况。 而相较于物理网络,移动环境普遍存在着稳定性差、带宽波动剧烈、信号覆盖不均衡导致网 络频繁切换等多个问题,加之外部环境的复杂性与多样性,通话延迟、卡顿、中断、回声与 噪声等问题难以避免,克服这些问题便成为了呼叫中心移动化所面临的巨大挑战与难点。 云服务篇 389 四、携程呼叫中心移动坐席解决方案 携程呼叫中心通过优化标准的 SIP 协议,减少坐席应用与后端服务的交互;深度定制音频编 解码器,在提高音质的同时降低了数据流量;通过丢包补偿技术来提升弱网环境下的通话质 量。 基于优化后的 SIP 协议开发标准的 SDK,应用层可快速实现电话相关的功能,将原来基于电 脑或 IP 电话的 IP-Talk 的方式移植到移动智能终端,突破传统的空间限制,实现了完全开放 的自由移动。 1、CCodec 音频编解码器,通俗理解就是把自然界的声音采集,转换成数字信号,再采用相应的压缩技 术,对得到的数字信号进行压缩,即可形成常见的音频文件,如 wav,mp3,aac 等。 而音频的数字化采集与处理,理论上无法实现完全与自然发音相同,只是尽可能优化算法, 使其最大化接近原始发音。同等条件下,音频质量主要取决于以下技术指标:  采样率:一秒内采样声音波形的点数,每秒抽取的点数越多,获取的频率信息就越丰富, 音频还原也就越接近自然。  采样位数:采样获取到模拟信号的数字表示,比特率越高,表示某一点的信息也就越丰 富。 经过相关的编码、压缩算法的处理,音质越高所产生的音频流越大,传输所需要的带宽也越 高,与之相对应的,耗费的流量也同比上涨。 移动场景下,音频编码及相关的压缩算法需要在提高音质保证用户体验的同时,尽可能降低 传输带宽和存储空间。如果音频编解码能支持动态码率,便能为用户带来更好的通话体验。 所谓动态码率支持,即:  在弱网情况下使用低码率,适当的降低音质,以便降低对带宽的要求及流量的消耗,以 提高通话的成功率和有效性。  在网络质量良好的情况下提高码率,相应的,音质也可以随之提升,甚至可以提高音频 的声道数,在提高通话良好体验的同时,可以支持音乐等更丰富的音频听感。 携程呼叫中心研发团队通过研究最近的音频编解码技术和相关压缩算法,研发完成了一套有 损音频编码器——CCodec。 CCodec 是基于开源音频编解码算法研发的有损音频编解码器,不仅可以支持动态调整比特 率、音频带宽和帧大小,同时能在编码的过程中根据音频数据的复杂程序即时确定使用的比 特率,在保证质量的前提下兼顾编码后产生文件的大小,即 VBR(Variable Bit Rate)。在保证 音频质量的同时,大大降低了数据流量,尤其适合互联网上的语音实时交互和音乐传输。 云服务篇 390 CCodec 可用于较多类型的音频应用,如 VoIP、视频会议、游戏内的语音聊天、基于实时的 音乐会直播等。其主要有以下特性:  支持多种比特率  支持 8kHz 到 48kHZ 的采样率  支持 CBR 和 VBR 两种码率技术  支持单声道和立体声  支持多声道  可以动态调整比特率、音频带宽和帧大小  具有较好的鲁棒性丢失率和丢包补偿机制 经过实验对比,除以上技术功能的支持外,它也具有良好的低算法延迟,非常适合实时通讯 类的应用。在平衡音质和比特率的情况下,算法延迟可进一步降低到 5ms。 质量比特率对比 从比特率与质量的对比曲线中可以看出,CCodec 编解码不仅在低比特时对音频的保真超越 了 iLBC、AMR-NB、Speex、AMR-WB,在高比特率的情况下,音质依然越超众多现有的编 解码。 在移动弱网场景下,可以设置 CCodec 比特率为 16-32,优先满足通话功能,进行有效沟通。 而当网络状态良好的情况下,可以使用 32 以上的比特率,以适应传输更高品质的音乐等丰 富的音频数据流。 云服务篇 391 比特率迟时对比 呼叫中心系统多用于处理用户的电话咨询或售后服务,属于实时通信系统。在实时通信系统 中,音频的延时对双方的沟通体验会造成巨大影响,而延时也是实时通讯系统中极为重要的 标准。因此,音频算法的延时显得尤为重要。 CCodec 编解码器算法延时小的特性,非常适合应用于携程电话、VoIP 或视频会议等应用场 景。 2、CSIP SIP 是由 IETF 制定的多媒体通信协议,它是一个基于文本的应用层控制协议,用于创建、修 改和释放一个或多个参与者的会话,广泛应用于 CS(Circuit Switched, 电路交换),NGN(Next Generation Network,下一代网络)以及 IMS(IP Multimedia Subsystem, IP 多媒体子系统)的网 络中,可支持并应用于语音、视频、数据等多媒体业务。 而 CSIP 是一个基于 SIP, SDP,RTP, STUN 等协议而实现的通信库,可以支持音频、视频及短 消息的传输。为适应移动网络的抖动、时延 ,CSIP 实现时加入了以下特性:  集成 CCodec 音频编解码,抗 30%网络丢包,支持 250ms 网络迟延。在弱网情况下,提 升通话体验  网络状态监控,动态调整比特率,保障移动网络下的稳定运行  断开自动恢复功能 云服务篇 392  优化回声消除、语音降噪的处理,提升听觉体验 3、Ctrip PhoneSDK 便捷接入 CSIP 功能强大且灵活,但由于是基于 C 语言开发,接口及使用方式对于移动开发来说,比 较复杂且不宜使用。PhoneSDK 在 CSIP 的基础上进行了逻辑封装,提供简单且易于使用的 接口 API。 考虑到呼叫中心的特殊场景,在 PhoneSDK 的基础上,团队进一步扩展了与坐席相关的功 能,实现了坐席的登录、状态改变等相关功能,进一步降低了坐席类应用的开发成本。 PhoneSDK 可快速实现以下网络电话相关的功能: 云服务篇 393  呼出/应答  挂起  转移  静音  多人会话  会议  网络状态监测  IPv6  WebSocket  P2P  TCP/SSL/UDP  噪声消除  回声抑制  丢包补偿  抗网络抖动  动态码率调整 流量对比 为了使用 PhoneSDK 适应移动场景下的音频传输,在音频压缩方面采用了有损压缩算法,在 保证音质的前提下,大大降低了编码后的音频大小。在相同的网络环境下,使用相同的设备 进行测试,在使用 CCodec 48KHz 采样率的情况下,编码的音频仅为微信的 1/2,是传统 G711/PCMu 的 1/3 左右。 五、案例- “十一贝”呼叫中心私有云 云服务篇 394 北京十一贝技术有限公司,主要侧重于保险营销业务,提供去哪儿等用户群不同类型的保险 产品。根据其业务特性及具体需求,以移动 App 为主题,利用 PhoneSDK 快速研发了一套 坐席 App,提供十一贝业务人员使用。 对于公司固定坐席人员来说,可通过 Wifi 网络,使用坐席 App 接入呼叫中心系统。在查询 待拜访的客户及适合的产品类型的同时,可通过 PhoneSDK 快速拨打用户电话,完成日常工 作。如果外出办公,也能与其他客户保持沟通,提供高效的一对一服务,不影响业务,真正 实现了坐席移动化。 系统于 2017 年 3 月正式上线,目前已开放北京、深圳、西安和广州的坐席,其他城市计划 陆续开放。 携程技术沙龙个性化推荐专场 395 携程技术沙龙个性化推荐专场 携程技术沙龙个性化推荐专场 396 饿了么推荐系统的从 0 到 1 [作者简介]陈一村 ,饿了么数据运营部资深算法工程师。2016 年加入饿了么,现从事大数 据挖掘和算法相关工作,包括推荐系统、用户画像等。 现场视频:https://v.qq.com/x/page/g03603xh216.html 随着移动互联网的发展,用户使用习惯日趋碎片化,如何让用户在有限的访问时间里找到想 要的产品,成为了搜索/推荐系统演进的重要职责。作为外卖领域的独角兽, 饿了么拥有百万 级的日活跃用户,如何利用数据挖掘/机器学习的方法挖掘潜在用户、增加用户粘性,已成 为迫切需要解决的问题。 个性化推荐系统通过研究用户的兴趣偏好,进行个性化计算,发现用户的兴趣点,从而引导 用户发现自己的信息需求。一个好的推荐系统不仅能为用户提供个性化的服务,还能和用户 之间建立密切关系,让用户对推荐产生依赖。 本次分享介绍饿了么如何从 0 到 1 构建一个可快速迭代的推荐系统,从产品形态出发,包 括推荐模型与特征工程、日志处理与效果评估,以及更深层次的场景选择和意图识别。 本文将就模型排序与特征计算的线上实现做具体说明,同时补充有关业务规则相关的洗牌逻 辑说明,力图从细节上还原和展示饿了么美食推荐系统。 一、模型排序 1.1 设计流程 对于任何一个外部请求, 系统都会构建一个 QueryInfo(查询请求), 同时从各种数据源提取 UserInfo(用户信息)、ShopInfo(商户信息)、FoodInfo(食物信息)以及 ABTest 配置信息 等, 然后调用 Ranker 排序。以下是排序的基本流程(如下图所示): 1. 调取 RankerManager, 初始化排序器 Ranker: 根据 ABTest 配置信息, 构建排序器 Ranker: 调取 ScorerManger, 指定所需打分器 Scorer(可以多个); 同时, Scorer 会从 ModelManager 获 取对应 Model, 并校验; 调取 FeatureManager, 指定及校验 Scorer 所需特征 Features; 2. 调取 InstanceBuilder, 汇总所有打分器 Scorer 的特征, 计算对应排序项 EntityInfo(餐厅/食 物)排序所需特征 Features; 3. 对 EntityInfo 进行打分, 并按需对 Records 进行排序; 携程技术沙龙个性化推荐专场 397 这里需要说明的是:任何一个模型 Model 都必须以打分器 Scorer 形式展示或者被调用。主 要是基于以下几点考虑: 模型迭代:比如同一个 Model,根据时间、地点、数据抽样等衍生出多个版本 Version; 模型参数:比如组合模式(见下一小节)时的权重与轮次设定,模型是否支持并行化等; 特征参数:特征 Feature 计算参数,比如距离在不同城市具有不同的分段参数; 下面将详细介绍 Ranker 初始化(1)与 Records 复杂排序逻辑(3); 有关特征计算(2), 详见下一 节特征计算。 1.2 排序逻辑 对于机器学习或者学习排序而言,多种模型的组合(Bagging, Voting 或 Boosting 等)往往 能够带来稳定、有效的预测结果。所以,针对当前美食推荐项目,框架结合 ABTest 系统, 支持 single、linear 及 multi 三种组合模式,具体说明如下: single:单一模式,仅用一个 Scorer 进行排序打分; linear:线性加权模式,指定一系列 Scorer 以及对应的权重, 加权求和; multi:多轮排序模式,每轮指定 Scorer, 仅对前一轮的 top N 进行排序; 具体说明如下: 1. 单一模式:rankType=single 携程技术沙龙个性化推荐专场 398 对于单一模式,仅有一个 Score,且不存在混合情况,所以只要简单对 Scorer 的打分进行排 序即可,故在此不做详细展开。ABTest 配置格式如下表: 2. 线性加权模式:rankType=linear 对于线性加权模式,在单一模式配置的基础上,需要在 ABTest 配置每个 Scorer 的权重,格 式如下表所示: 当 LinearRanker 初始化时,会校验和初始化所有打分器 Scorer。之后,按照以下步骤对餐厅 /食物列表进行排序,详见下图(左): 特征计算器 InstanceBuilder 调用 ScorerList,获取所有所需特征 Feature 并去重; InstanceBuilder 对所有餐厅/食物进行特征计算,详见特征计算; ScorerList 中所有 Scorer 对所有餐厅/食物依次进行打分; 对所有 Scorer 打分进行加权求和,之后排序; 3. 多轮排序模式:rankType=multi 对于多轮排序模式,每轮设定一个 Scorer,对前一轮 top=Num 个餐厅/食物进行排序,故在 ABTest 中需要设定每个 Scorer 的轮次(round)和排序数(num),格式如下表。 携程技术沙龙个性化推荐专场 399 MultiRanker 初始化与特征计算与 LinearRanker 类似,具体步骤详见上图(右): 特征计算器 InstanceBuilder 调用 ScorerList,获取所有所需特征 Feature 并去重; InstanceBuilder 对所有餐厅/食物进行特征计算,详见特征计算; Scorer 按轮次(round)对 top=Num 餐厅/食物进行打分; 对 top=Num 餐厅/食物按当前 Scorer 的打分进行排序; 重复步骤 3、4,直到走完所有轮次; 在初始化阶段,Ranker 根据 ABTest 配置信息指定算法版本(algoVersion)、排序类型 (rankType)、排序层级(rankLevel)及相关打分器(ScorerList)。 1.3 模型定义 对于线上任何 Model, ModelManager 都会通过以下流程获取相应实例和功能(如下图所示): 模型实例化时的构造函数 BaseModel()和校验函数 validate(); 通过FeatureManager获取对应Model的特征Feature:abstractgetFieldNames()/getFeatures(); 传入 Model 的特征,获取预测分数:abstract predict(Mapvalues) 和 abstract predict(List values); 对于 Model 的迭代和更新、以及之后的 Online Learning 等,通过 ModelManager 对接相应 服务来实现。 携程技术沙龙个性化推荐专场 400 如上图所示,对于任何一个可被 Scorer 直接调用 Model,都需要实现以下接口: 可供 ModelManager 进行 Model 实例化的 BaseModel() 和初始化的 init(); 可供 Scorer/InstanceBuilder 获取特征项的 getFieldNames()/getFeatures(); 可供 Scorer 调用进行打分的 predict(Map values) 和 predict(List values); 二、特征计算 2.1 设计流程 不同于离线模型训练,线上特征计算要求低延迟、高复用、强扩展,具体如下: 低延迟:针对不同请求 Query,能够快速计算当前特征值,包括从各种 DB、Redis、ES 等数 据源实时地提取相关数据进行计算; 携程技术沙龙个性化推荐专场 401 高复用:对于同类或者相同操作的特征,应该具有高复用性,避免重复开发,比如特征交叉 操作、从 USER/SHOP 提取基本字段等; 强扩展:能够快速、简单地实现特征,低耦合,减少开发成本; 根据以上系统设计要求,下图给出了特征计算的设计流程和特征基类说明。 具体说明如下: FeatureManager:特征管理器,用于特征管理,主要功能如下: 特征管理:包括自定义特征、基础特征、实时特征、复合特征等; 特征导入:自定义特征静态代码注册, 其他特征数据库导入; 特征构建:CompsiteFeature 类型特征构建; InstanceBuilder:特征构建器,用于计算餐厅/食物特征,具体步骤如下: 从每个 Scorer 获取 Feature 列表,去重,依赖计算,最后初始化; 携程技术沙龙个性化推荐专场 402 层级、并行计算每个 EntityInfo 的特征值;(之后会考虑接入 ETL,用于 OnlineLearning) 2.2 特征定义 上图给出了特征基类说明, 以下是具体的字段和方法说明: type: 特征类型,现有 query、shop、food, 表示 Feature 的特征维度(粒度) operate: CompositeFeature 专属, 特征操作, 指定当前特征行为, 比如 ADD、MAPGET 等 name: 特征名称 weight: 权重, 简单线性模型参数 retType: 特征返回字段类型 defValue: 特征默认返回值 level:CompositeFeature 专属, 当前特征层次, 用 于 特 征 层 次 计 算 operands:CompositeFeature 专属, 特征操作数, 前置特征直接依赖 dependencies: CompositeFeature 专属, 特征依赖 *init():特征初始化函数 *initOther(QueryInfo):InstanceBuilder 调用时实时初始化, 即传入当前特征参数 *evaluate(QueryInfo, EntityInfo, StringBuilder): 用于餐厅/食物维度的特征计算 根据上两小节设计流程和基类定义的说明, 我们能够非常快速、简便地实现一个自定义特征, 具体流程如下(score 为例, 对应类名 XXXFeature): 特征类实现: 建立 XXXFeature, 并继承 BaseFeature/CompositeFeature; 实现 init(), 设置 type\name(defValue\weight 可选)等; 实现 initOther(), 设置特征参数, 包括 infoMap; 实现 evaluate(), 具体包括特征计算的详细逻辑, 对于返回数值的特征; 特征注册: 在 FeatureManager 中注册, 或者在后台特征管理系统中注册; 考虑到代码中不允许出现明文常量, 故需在 FeatureConsts 中添加常量定义 2.3 特征分类 1. 基础特征:基础特征为线上可以通过配置特征名直接从 SHOP/USER 获取特征值的特征, 比如:shop_meta_、user_meta_、food_meta_等, 详细说明如下表,其从本质上来讲等同于 特征操作符(复合特征)。 携程技术沙龙个性化推荐专场 403 2. 实时特征:实时特征来源于 Kafka 与 Storm 的日志实时计算, 存于 Redis,比如:用户食物 搜索与点击信息, 实例如下表。 3. 自定义特征:线上除 CompositeFeature 特征外, 所有 XXXFeature 均为自定义特征, 再次 不再累述. 4. 复合特征(CompositeFeature):用户特征组合的复杂操作, 比如下表所示(部分) 携程技术沙龙个性化推荐专场 404 三、洗牌逻辑 3.1 洗牌类型 很多时候, 基于算法模型的结果能够给出数据层面的最佳结果, 但是不能保证推荐结果符合 人的认知, 比如基于 CTR 预估的逻辑, 在结果推荐上会倾向于用户已点过或已购买过的商户 /食物, 这样就使得推荐缺少足够的兴趣面。所以, 为了保证推荐结果与用户的相关性, 我们 会保留算法模型的结果; 同时, 为了保证结果符合认知, 我们会人为地添加规则来对结果进 行洗牌; 最后, 为了扩展用户兴趣点、引导用户选择, 将会人工地引入非相关商户/食物, 该 部分将是我们后续优化点之一。下面将详细介绍“猜你喜欢”模块线上生效的部分洗牌逻 辑,其他洗牌规则类似。 1、餐厅类目洗牌: 携程技术沙龙个性化推荐专场 405 考虑到餐厅排序时, 为避免同类目餐厅扎堆问题, 我们设定了餐厅类目洗牌, 基本规则如下: 针对 top =SHOP_CATE_TOPNUM 餐厅, 不允许同类目餐厅连续超过 MAX_SHOP_SHOPCNT。 2、餐厅推荐食物数洗牌: 在餐厅列表排序时, 总是希望排在前面的商户具有更好的展示效果、更高的质量。针对 1*餐 厅+3*食物 模式, 如果前排餐厅食物缺失(少于 3 个)时, 页面的整体效果就会大打折扣, 所 以我们制定了食物数洗牌, 具体规则如下: 所有 1 个食物的餐厅沉底 针对top=SHOP_FOODCNT_TOPNUM餐厅, 食物数 < SHOP_FOODCNT_FOODCNT(3) 的餐 厅降权 3、餐厅名称洗牌: 正常时候, 推荐需要扩展和引导用户的兴趣点, 避免同类扎堆, 比如盖浇饭类目餐厅等。同 样的, 我们也不希望相同或相似名称的餐厅扎堆, 比如连锁店、振鼎鸡等。针对此问题, 考虑 到 餐 厅 名 称 的 不 规 则 性, 我 们 通 过 分 词 和 统 计 , 把所有餐厅名称做了结构化归类 (distinct_flag), 比如所有“XXX 黄焖鸡”都归为“黄焖鸡”、“星巴克 XX 店”归为“星巴 克”等。之后类似于餐厅类目洗牌, 做重排, 具体规则如下: 对 top=SHOP_FLAG_TOPNUM 餐厅进行标签(flag)洗牌, 使得同一标签的餐厅排序位置差不 得小于 SHOP_FLAG_SPAN 3.2 线上逻辑 从上一节中可知, 各个洗牌之间存在相互制约, 即洗牌不能并行、只能串行, 谁前谁后就会 导致不同的排序结果, 所以, 这里需要考虑各个洗牌对排序的影响度和优先级: 影响度:即对原列表的重排力度, 比如对于连锁店少的区域, 名称洗牌的影响度就会小, 反 之, 比如公司周边有 25 家振鼎鸡, 影响度就会变大; 优先级:即洗牌的重要性, 比如前排餐厅如果食物少于规定数量, 其实质是浪费了页面曝光 机会, 所以食物数洗牌很有必要; 考虑到洗牌的串行逻辑, 越靠后的洗牌具有更高优先级。为了能够灵活变更线上的洗牌规则, 系统结合 Huskar System(线上配置修改系统), 能够快速、便捷地更改洗牌逻辑,下面给出了 一个配置实例。 [ {"name": "recfoods", "topnum": 15,"foodcnt": 3}, {"name": "category", "topnum": 15,"shopcnt": 2}, 携程技术沙龙个性化推荐专场 406 {"name": "shopflag", "topnum": 20,"span": 3, "exclude": "XXX"}}, {"name": "recfoods", "topnum": 15,"foodcnt": 3}, {"name": "dinner","topnum":5,"interval":["10:30~12:30","16:30~18:30"]}, {"name": "mixture","topnum":12, "include":"XXX"} ] 四、总结 对于一个处于业务快速增长期的互联网企业,如何能够在最短时间内构建一个可快速迭代的 推荐系统,是摆在眼前的现实问题。 本次分享,从饿了么自身业务出发,结合推荐系统的常见问题和解决方案,给出了从产品形 态出发, 包括推荐模型与特征工程、日志处理与效果评估,以及更深层次的场景选择和意图 识别等在内多方面的线上实践,力图从整体及细节上还原和展示推荐系统的本质,希望能够 对大家的工作有所启发。 携程技术沙龙个性化推荐专场 407 腾讯云推荐引擎实践 [作者简介]吕慧伟,腾讯云布道师,腾讯社交网络运营部高级工程师,腾讯通用推荐系统神 盾开发负责人,腾讯云推荐引擎架构师。中国科学院计算技术研究所博士,美国阿贡国家实 验室博士后,从事并行计算多年,MPICH 核心开发者之一。 现场视频:https://v.qq.com/x/page/y0360imcmxm.html 我们每个人每天都会使用到不同的推荐系统,无论是听歌,购物,看视频,还是阅读新闻, 推荐系统都可以根据你的喜好给你推荐你可能感兴趣的内容。不知不觉之间,推荐系统已经 融入到我们的生活当中。作为大数据时代最重要的几个信息系统之一,推荐系统主要有下面 几个作用: 提升用户体验。通过个性化推荐,帮助用户快速找到感兴趣的信息。 提高产品销售。推荐系统帮助用户和产品建立精准连接,从而提高产品转化率。 发掘长尾价值。根据用户兴趣推荐,使得平时不是很热门的商品可以销售给特定的人群。 方便移动互联网用户交互。通过推荐,减少用户操作,主动帮助用户找到他感兴趣的内容。 以应用宝为例,对于两个不同用户 A 和 B,打 开应用程序的界面是很不一样的。用户 A 是一 个年轻男性用户,平时可能喜欢玩手机游戏和看小说,所以应用宝的推荐系统会给他推荐游 戏的应用。而用户 B 是一个年轻女性用户,平时喜欢购物和轻游戏,所以应用的推荐系统就 会给她推荐购物的应用。 这样一来,从用户的角度,减少了他找到自己喜欢的应用的时间;从产品的角度,用户更愿 意去点击和安装他喜欢的应用,所以提高了产品的转化率。 图 1. 应用宝首页界面 除了应用宝之外,腾讯云推荐系统还应用在腾讯的 QQ 空间、QQ、企鹅 FM、QQ 会员和黄 携程技术沙龙个性化推荐专场 408 钻贵族等 12 个不同的业务的 200 多个不同推荐场景,每天处理的推荐请求有上百亿个。那 么,这个日均百亿级请求的推荐系统是怎么打造而成的呢?主要需要解决两个问题: 支持众多业务和场景。 支持海量用户请求。 一、通用化推荐算法库 首先要解决的问题是如何支持众多业务和场景。对于不同的场景,用到的数据、算法和模型 都会有很多的不同,如果对于每个场景都从头开发,将会耗费非常多的时间和人力。那么有 没有更好的方法呢?毕竟常用的推荐算法就是那么几种,有没有一种方法使得同一个推荐算 法可以复用到不同的推荐场景呢?那就需要对推荐算法库进行通用化设计。 下面举一个例子来说明推荐系统是什么,又是怎么工作的。 如图 2 所示,一个推荐系统是由学习系统、模型和推荐系统三部分组成的。其中,学习系统 通过机器学习的方法对用户的历史数据进行统计、分析,从而训练得到一个模型。这个模型 是用户行为规律的总结,会在后面预测系统中对新用户的请求进行预测。 比如图中简单的例子,学习系统的输入是 5 个不同用户的行为,对于男性用户 A,他喜欢的 《王者荣耀》这个游戏,对于女性用户 B,她喜欢的则是《奇迹暖暖》,那么对于这 5 个用 户统计得到的模型是男性用户喜欢《王者荣耀》的概率是 0.67,而女性用户喜欢《奇迹暖暖》 的概率是 1。有了这个简单的模型以后,如果在预测系统中有一个新的用户请求,来自一个 男性用户,那么按照前面的模型,会按照概率的大小,把《王者荣耀》推荐给这个用户。 图 2. 推荐系统例子 实际的推荐系统中,学习系统处理的用户数据量会更大,数据的维度也更多,用到的推荐模 型也会更复杂,常用的有协同模型、内容模型和知识模型。其中,协同模型主要通过我的朋 友喜欢什么来猜测我喜欢这么;内容模型则是根据物品本身来预测用户喜欢 A 所以也可能 喜欢 B;知识模型则是根据用户的限定条件,按照他的需要进行推荐。 携程技术沙龙个性化推荐专场 409 图 3. 通用化推荐算法库 一个常见的推荐系统由下面四个部分组成:样本库、特征库、 算法和模型。其中,样本库 存储从流水日志中提取的用户行为和特征;特征库存储用户和物品的属性等特征;算法是用 于训练模型用到的机器学习算法;模型库存储的是从样本和特征计算得到的训练模型。为了 不同的算法可以用于不同的样本和特征,我们可以使用图 3 中的算法配置表来存储数据、算 法和模型的映射关系,将模型、算法、样本和特征的关系解耦,使得算法可以复用。比如, 我的模型是从样本 1 和特征 1 使用算法 A 训练得到。 图 4. 推荐系统的离线和在线计算分工 学习系统训练一个模型一般会花比较长的时间,这部分我们称为离线计算,对实时性要求并 不高,比如,可以在几个小时的时间内计算出来,重要的是模型的质量。而预测系统则不一 样,因为预测系统是直接面向用户请求,所以要求它响应快,同时必须能够处理海量用户的 请求,系统必须稳定可靠。接下去我们会使用一个实时计算平台来满足这部分的要求。 二、面向海量在线服务的实时计算平台 除了前面提到的通用化算法库,我们需要解决的第二个问题是如何处理海量的用户请求。这 携程技术沙龙个性化推荐专场 410 部分我们用的是一个名为 R2 的面向海量在线服务的自研实时计算平台。R2 有下面几个特 点: 海量,目前在 R2 系统上,每天处理上百亿的个性化推荐请求; 实时,每个请求的处理平均延时为 18ms; 可靠,系统稳定性为 99.99%。 R2 从一开始就是围绕线上服务而设计。首先,为了快速处理海量请求,我们参考了 Apache Storm,把 R2 定义成一个流处理框架。其次,为了系统的高可用性,在设计的时候,考虑了 系统不会出现单点故障。第三,为了方便扩容,计算资源是可插拔的。第四,系统可以支持 动态调度以方便负载平衡。第五,为了方便运维,还紧密结合运维工具提供告警和监控。 我们先来看一下 R2 这个流处理框架是如何处理推荐请求的。如图 5 所示的推荐场景用于猜 测用户喜欢的手机应用,可以分为三步来计算: 根据 id 得到用户特征; 使用决策树判断喜欢某个应用的概率; 对结果重新排序。其中,使用决策树这一步因为计算量大,可以通过并行计算来缩短处理时 间,每个处理单元可以处理子树的一部分,最后在第三步将结果汇总以后重新排序。 图 5. 流计算场景:猜测用户喜欢的手机应用 R2 的架构如图 6 所示,分为业务层、通信层和全局配置层三层。其中,业务层负责业务处 理逻辑;通信层负责基于名字的通信和分布式流处理;全局配置层负责可适应的拓扑配置、 动态扩缩容和动态负载调度。 携程技术沙龙个性化推荐专场 411 图 6. 实时计算平台 R2 架构 其中,业务层由两部分组成:计算拓扑图和计算单元(PU)。计算拓扑图负责数据的流向, 而计算单元负责业务的逻辑计算。通信层负责 PU 之间的通信以及拓扑图执行跟踪。其中, Interface 负责从业务接入请求,Acker 负责跟踪拓扑图执行情况,而 R2Server 负责转发 PU 之间的通信、启动 PU 和监控 PU 心跳。PU 之间的通信都通过 R2Server 转发。全局配置层 负责拓扑管理和名字服务,通过 Zookeeper 来进行动态配置。其中,拓扑管理将逻辑拓扑映 射到物理拓扑,名字服务提供 PU 地址查询。 三、腾讯云推荐引擎 基于上面的经验,我们打造了腾讯云推荐引擎。腾讯云推荐引擎(CRE)是面向广大中小互 联网企业打造的一站式云推荐引擎解决方案,提供安全、便捷、精准、可靠的推荐系统服务, 提升其业务的点击转化率和用户体验。 如图 7 所示,CRE 由算法模型和在线计算两部分组成。算法模型部分由于采用了通用推荐算 法库设计,用户在接入推荐场景时只需要通过简单配置,就可以直接使用已有的算法模版。 在线计算部分集成了 R2 的优势,系统稳定可靠,并且支持快速扩容。 携程技术沙龙个性化推荐专场 412 图 7. 腾讯云推荐引擎 腾讯云推荐引擎具有下面的功能: 一天接入,快速上线; 模板化算法,节省 99%代码; 快速扩容,应对业务快速增长; 稳定可靠,节省运维开销。 这些功能降低了推荐系统的技术门槛,使得搭建推荐系统变得简单便捷。 四、总结 综上所述,要打造一个百亿级通用推荐系统,需要考虑下面几点: 1.为了能够支持尽可能多的业务和场景,推荐算法库需要做通用化设计。 2.为了支撑海量在线用户的实时请求,实时计算平台必须低延时,可扩展,而且稳定可靠。 3.云推荐引擎的解决方案,在通用化的基础上,同时考虑了易用性,方便用户接入。 携程技术沙龙个性化推荐专场 413 推荐系统中基于深度学习的混合协同过滤模型 [作者简介]董鑫,携程基础业务部 BI 团队高级算法工程师,博士毕业于上海交通大学计算机 科学与技术系。 现场视频:https://v.qq.com/x/page/x0360zpd2g5.html 近些年,深度学习在语音识别、图像处理、自然语言处理等领域都取得了很大的突破与成就。 相对来说,深度学习在推荐系统领域的研究与应用还处于早期阶段。 携程在深度学习与推荐系统结合的领域也进行了相关的研究与应用,并在国际人工智能顶级 会议 AAAI 2017 上发表了相应的研究成果《A Hybrid Collaborative Filtering Model with Deep Structure for Recommender Systems》,本文将分享深度学习在推荐系统上的应用,同时介 绍携程基础 BI 团队在这一领域上的实践。 一、推荐系统介绍 推荐系统的功能是帮助用户主动找到满足其偏好的个性化物品并推荐给用户。推荐系统的输 入数据可以多种多样,归纳起来分为用户(User)、物品(Item)和评分(Ratings)三个层面,它们 分别对应于一个矩阵中的行、列、值。对于一个特定用户,推荐系统的输出为一个推荐列表, 该列表按照偏好得分顺序给出了该用户可能感兴趣的物品。 图 1. 推荐系统问题描述 如图 1 右边所示,推荐问题一个典型的形式化描述如下:我们拥有一个大型稀疏矩阵,该矩 阵的每一行表示一个 User,每一列表示一个 Item,矩阵中每个“+”号表示该 User 对 Item 携程技术沙龙个性化推荐专场 414 的 Rating,(该分值可以是二值化分值,喜欢与不喜欢;也可以是 0~5 的分值等)。 现在需要解决的问题是:给定该矩阵之后,对于某一个 User,向其推荐那些 Rating 缺失的 Item(对应于矩阵中的“?”号)。 有了如上的形式化描述之后,推荐系统要解决的问题归结为两部分,分别为预测(Prediction) 与推荐(Recommendation)。 “预测”要解决的问题是推断每一个 User 对每一个 Item 的偏爱程度,“推荐”要解决的 问题是根据预测环节所计算的结果向用户推荐他没有打过分的 Item。 但目前绝大多数推荐算法都把精力集中在“预测”环节上,“推荐”环节则根据预测环节计 算出的得分按照高低排序推荐给用户,本次分享介绍的方案主要也是”预测”评分矩阵 R 中 missing 的评分值。 二、基于协同过滤的推荐 基于协同过滤的推荐通过收集用户过去的行为以获得其对物品的显示或隐式信息,根据用户 对物品的偏好,发现物品或者用户的相关性,然后基于这些关联性进行推荐。 其主要可以分为两类:分别是 memory-based 推荐与 model-based 推荐。其中 memory- based 推荐主要分为 Item-based 方法与 User-based 方法。协同过滤分类见图 2。 图 2. 协同过滤分类 Memory-based 推荐方法通过执行最近邻搜索,把每一个 Item 或者 User 看成一个向量,计 算其他所有 Item 或者 User 与它的相似度。有了 Item 或者 User 之间的两两相似度之后,就 可以进行预测与推荐了。 携程技术沙龙个性化推荐专场 415 图 3. 矩阵分解示意图 Model-based 推荐最常见的方法为 Matrix factorization,其示意图见图 3 左边。矩阵分解通 过把原始的评分矩阵 R 分解为两个矩阵相乘,并且只考虑有评分的值,训练时不考虑 missing 项的值,如图 3 右边所示。R 矩阵分解成为 U 与 V 两个矩阵后,评分矩阵 R 中 missing 的值 就可以通过 U 矩阵中的某列和 V 矩阵的某行相乘得到。矩阵分解的目标函数见图 3,U 矩阵 与 V 矩阵的可以通过梯度下降(gradient descent)算法求得,通过交替更新 u 与 v 多次迭代收 敛之后可求出 U 与 V。 矩阵分解背后的核心思想,找到两个矩阵,它们相乘之后得到的那个矩阵的值,与评分矩阵 R 中有值的位置中的值尽可能接近。这样一来,分解出来的两个矩阵相乘就尽可能还原了评 分矩阵 R,因为有值的地方,值都相差得尽可能地小,那么 missing 的值通过这样的方式计 算得到,比较符合趋势。 协同过滤中主要存在如下两个问题:稀疏性与冷启动问题。已有的方案通常会通过引入多个 不同的数据源或者辅助信息(Side information)来解决这些问题,用户的 Side information 可 以是用户的基本个人信息、用户画像信息等,而 Item 的 Side information 可以是物品的 content 信息等。例如文献[1]提出了一个 Collective Matrix Factorization(CMF)模型,如图 4 所示。 携程技术沙龙个性化推荐专场 416 图 4. Collective Matrix Factorization 模型 CMF 模型通过分别分解评分矩阵 R,User 的 side information 矩阵,Item,的 side information 矩阵,其中 User 或者 Item 出现在多个矩阵中,其所分解的隐向量都是一致的。 三、深度学习在推荐系统中的应用 Model-based 方法的目的就是学习到 User 的隐向量矩阵 U 与 Item 的隐向量矩阵 V。我们 可以通过深度学习来学习这些抽象表示的隐向量。 Autoencoder(AE)是一个无监督学习模型,它利用反向传播算法,让模型的输出等于输入。 文献[2]利用 AE 来预测用户对物品 missing 的评分值,该模型的输入为评分矩阵 R 中的一行 (User-based)或者一列(Item-based),其目标函数通过计算输入与输出的损失来优化模型,而 R 中 missing 的评分值通过模型的输出来预测,进而为用户做推荐,其模型如图 5 所示。 携程技术沙龙个性化推荐专场 417 图 5. Item-based AutoRec 模型 Denoising Autoencoder(DAE)是在 AE 的基础之上,对输入的训练数据加入噪声。所以 DAE 必须学习去除这些噪声而获得真正的没有被噪声污染过的输入数据。因此,这就迫使编码器 去学习输入数据的更加鲁棒的表达,通常 DAE 的泛化能力比一般的 AE 强。Stacked Denoising Autoencoder(SDAE)是一个多层的 AE 组成的神经网络,其前一层自编码器的输出作为其后 一层自编码器的输入,如图 6 所示。 图 6. SDAE 文献[3]在 SDAE 的基础之上,提出了 Bayesian SDAE 模型,并利用该模型来学习 Item 的隐 向量,其输入为 Item 的 Side information。该模型假设 SDAE 中的参数满足高斯分布,同时 携程技术沙龙个性化推荐专场 418 假设 User 的隐向量也满足高斯分布,进而利用概率矩阵分解来拟合原始评分矩阵。该模型 通过最大后验估计(MAP)得到其要优化的目标函数,进而利用梯度下降学习模型参数,从而 得到 User 与 Item 对应的隐向量矩阵。其图模型如图 7 所示。 图 7. Bayesian SDAE for Recommendation System 在已有工作的基础之上,携程基础 BI 算法团队通过改进现有的深度模型,提出了一种新的 混合协同过滤模型,并将其成果投稿与国际人工智能顶级会议 AAAI 2017 并被接受。该成果 通过利用 User 和 Item 的评分矩阵 R 以及对应的 Side information 来学习 User 和 Item 的隐 向量矩阵 U 与 V,进而预测出评分矩阵 R 中 missing 的值,并为用户做物品推荐。 图 8. Additional Stacked Denoising Autoencoder(aSDAE) 携程技术沙龙个性化推荐专场 419 该成果中提出了一种 Additional Stacked Denoising Autoencoder(aSDAE)的深度模型用来学 习 User 和 Item 的隐向量,该模型的输入为 User 或者 Item 的评分值列表,每个隐层都会接 受其对应的 Side information 信息的输入(该模型灵感来自于 NLP 中的 Seq-2-Seq 模型,每 层都会接受一个输入,我们的模型中每层接受的输入都是一样的,因此最终的输出也尽可能 的与输入相等),其模型图见图 8。 结合 aSDAE 与矩阵分解模型,我们提出了一种混合协同过滤模型,见图 9 所示。该模型通 过两个 aSDAE 学习 User 与 Item 的隐向量,通过两个学习到隐向量的内积去拟合原始评分 矩阵 R 中存在的值,其目标函数由矩阵分解以及两个 aSDAE 的损失函数组成,可通过 stochastic gradient descent(SGD)学习出 U 与 V,详情大家可以阅读我们的 paper《AHybrid Collaborative Filtering Model with Deep Structure for Recommender Systems》[4]。 图 9. 混合协同过滤模型 我们利用 RMSE 以及 RECALL 两个指标评估了我们模型的效果性能,并且在多个数据集上和 已有的方案做了对比实验。实验效果图如图 10 所示,实验具体详情可参看我们的 paper。 携程技术沙龙个性化推荐专场 420 图 10. 实验效果对比 在今年的推荐系统顶级会议 RecSys 上,Google 利用 DNN 来做 YouTube 的视频推荐[5],其 模型图如图 11 所示。通过对用户观看的视频,搜索的关键字做 embedding,然后在串联上 用户的 side information 等信息,作为 DNN 的输入,利用一个多层的 DNN 学习出用户的隐 向量,然后在其上面加上一层 softmax 学习出 Item 的隐向量,进而即可为用户做 Top-N 的 推荐。 图 11. YouTube 推荐模型图 此外,文献[6]通过卷积神经网络(CNN)提出了一种卷积矩阵分解,来做文档的推荐,该模型 结合了概率矩阵分解(PMF)与 CNN 模型,图见图 12 所示。该模型利用 CNN 来学习 Item 的 携程技术沙龙个性化推荐专场 421 隐向量,其对文档的每个词先做 embedding,然后拼接所有词组成一个矩阵 embedding 矩 阵,一篇文档即可用一个二维矩阵表示,其中矩阵的行即为文档中词的个数,列即为 embedding 词向量的长度,然后在该矩阵上做卷积、池化以及映射等,即可得到 item 的隐 向量。User 的隐向量和 PMF 中一样,假设其满足高斯分布,其目标函数由矩阵分解以及 CNN 的损失函数组成。 图 12. 卷积矩阵分解模型 四、总结 本文介绍了一些深度学习在推荐领域的应用,我们发现一些常见的深度模型(DNN, AE, CNN 等)都可以应用于推荐系统中,但是针对不同领域的推荐,我们需要更多的高效的模型。随 着深度学习技术的发展,我们相信深度学习将会成为推荐系统领域中一项非常重要的技术手 段。 引用: [1] Ajit P. Singh, Geoffrey J.Gordon. “Relational Learning via Collective Matrix Factorization”, KDD 2008 [2] SuvashSedhain, Aditya Krishna Menon, Scott Sanner, Lexing Xie. “AutoRec: AutoencodersMeet Collaborative Filtering”, WWW 2015 [3] Hao Wang , NaiyanWang, Dit-Yan Yeung. “Collaborative Deep Learning for Recommender Systems”, KDD 2015 [4] Xin Dong,Lei Yu, ZhonghuoWu, Yuxia Sun, Lingfeng Yuan, Fangxi Zhang. “A HybridCollaborative Filtering Model with Deep Structure for Recommender Systems”, AAAI 2017 [5] PaulCovington, Jay Adams, Emre Sargin. “Deep Neural Networks for YouTubeRecommendations”, RecSys 2016 [6] DonghyunKim, Chanyoung Park, Jinoh Oh, Sungyoung Lee, Hwanjo Yu. “Convolutional 携程技术沙龙个性化推荐专场 422 MatrixFactorization for Document Context-Aware Recommendation”, RecSys 2016 携程技术沙龙个性化推荐专场 423 跨领域推荐,实现个性化服务的技术途径 [作者简介]曹健,上海交通大学计算机系教授。近年来在大数据智能分析领域进行研究与应 用。 现场视频:https://v.qq.com/x/page/s03613f73dd.html 一、跨领域推荐的概念 推荐系统在我们这个时代扮演了越来越重要的角色。如何利用海量数据,来对用户的行为进 行预测,向用户推荐其感兴趣的物品与服务成为各大互联网公司非常关注的问题。 目前学术界与工业界对推荐的研究与应用,主要集中在对单领域的个性化推荐,即根据用户 对某一领域(如书籍)的兴趣特点和购买行为,向用户推荐用户感兴趣的信息和商品。 领域反映了两组对象相互间的关系,比如用户对书籍的评价数据即可看作一个用户-书籍领 域,而这个领域本身可以用一个用户-书籍的评分矩阵表示,其中的第 i 行 j 列的值,即用户 i 对书籍 j 的评分信息。 单领域即表示输入的数据只有一组二元关系,它可以是评分关系(如用户对书籍的评分), 购买关系(如用户是否购买书籍,可以用一个 0-1 评分矩阵表示,其中的 1 表示该用户购买 该书籍,0 表示未购买),也可以是用户或物品的特征关系(比如其中一个维度是用户,另 一个维度是用户的年龄、性别等)等。 推荐系统根据用户对某一领域(如书籍)的兴趣特点和购买行为,向用户推荐用户感兴趣的 信息和商品。推荐可以大大节省用户筛选信息的时间,得以从广大信息中获取其感兴趣的, 对自己有价值的信息。 现有的单领域个性化推荐大多基于协同过滤推荐方法。协同过滤推荐方法的主要思想是,利 用已有用户群过去的行为或意见预测当前用户最可能喜欢哪些东西或者对哪些东西感兴趣。 单领域协同过滤推荐在过去取得了很好的效果,包括 Amazon 公司在内的很多互联网公司 都采用了这样的协同过滤推荐。 然而,单领域的个性化推荐存在一些问题和局限,主要表现在以下几个方面: 1、单领域推荐经常面临数据稀疏问题 单领域推荐数据往往过于稀疏(Sparsity),因而难以通过训练样本获得好的推荐效果。以用 户购买书籍这个领域为例,对于用户来说,他在某个网站购买的书的数量必然是有限的,绝 大多数用户只在网站购买了一两本书籍,而这个网站的所有出售的书籍可能有上百万本。如 果仅根据这样的数据进行推荐,效果肯定不会理想。 携程技术沙龙个性化推荐专场 424 2、单领域推荐经常遇到冷启动问题 对某个领域的新用户,难以进行推荐,即冷启动(Cold-start)问题。还以用户购买书籍为 例,因为是单领域推荐问题,对在该领域从未购买过书籍的用户来说,系统没有该用户在这 个领域的任何信息,因此必然无法对用户进行单领域个性化推荐,只能推荐给用户一些流行 热门的商品,无法体现个性化。 3、单领域推荐难以实现“真正个性化”推荐 所谓的个性化推荐,在单领域往往是“群体分类推荐”。因为用户在单领域所留下的信息有 限,比如可能只购买过两本书,而很多用户都可能只是购买了相同的这两本书,对这些购买 过相同书的用户,因为已知信息完全相同,所以推荐给他们的肯定也是相同的书籍。从这一 角度来看,并不能真正做到个性化推荐。 跨领域推荐是将多个领域数据联合起来,共同作用于目标领域推荐。比如一个系统拥有用户 -书籍和用户-电影评价数据。关于这个问题跨领域推荐的做法,是在考虑用户-书籍评分信 息的同时,也同时考虑用户-电影评分信息以及其他可获得且可能有益用户-书籍推荐的信 息,综合起来对用户进行书籍的推荐。 目前跨领域推荐成为了学术界的一个研究热点,工业界如 Facebook 也采用了跨领域技术进 行推荐。跨领域推荐有如下优势: 1、解决单领域推荐数据稀疏问题 因为是跨领域推荐,推荐可利用的数据大大增加,其他领域数据可以更好地为主领域推荐提 供帮助。同样以用户购买书籍为例,仅看用户购买书籍,所能利用的数据极其有限。然而, 可能还有很多其他的数据可以被利用。比如用户购买电影、音乐等的购买数据,以及用户所 浏览过的所有书籍(却未购买)的数据,用户在书籍页面停留的时间数据乃至用户的个人信 息数据等等数据,有助于解决主领域的数据稀疏问题。 2、解决单领域推荐冷启动问题 对主领域的新用户,可以通过其他领域的数据来对用户进行个性化推荐,解决冷启动问题。 比如对一个大型网站来说,一个新用户从未购买过书籍,但系统可能拥有该用户的其他信息。 比如该用户购买的电影和音乐信息,以及该用户浏览过的书籍商品页面数据。这些数据能帮 助系统对该用户进行书籍购买的推荐。 3、跨领域推荐未来或将实现“真正个性化”推荐 因为系统拥有除主领域外的其他领域的海量数据,可以获取到包括用户生活习惯在内的各种 数据。可以说,每个用户都不再类似,而是独一无二的个体。基于这样的现状,即使一个用 户只购买过一两件商品,也可以联合其他数据,对用户进行真正的个性化推荐。 二、跨领域推荐方法的现状 携程技术沙龙个性化推荐专场 425 在现有的跨领域个性化推荐方法中,多数采用的是基于协同过滤的推荐方法,还有部分采用 了迁移学习和基于知识的跨领域推荐方法。下面将介绍现有的各种方法。 2.1 基于协同过滤的跨领域推荐 2.1.1 基于矩阵合并的协同过滤 假设一个跨领域推荐场景 1:有一家类似豆瓣的社区门户网站,数据包含该网站用户对于不 同电影的评分矩阵푋푀,大小为푢 × 푚。以及用户对不同书籍的评分矩阵푋퐵,大小为푢 × 푏。 用户-电影和用户-书籍评分矩阵即可看作两个领域。因为用户-电影评分很稀疏,所以希望 利用已有的多领域信息,对电影推荐提供帮助。 一种最直观的想法是通过把多领域用户-评分矩阵合并为一个用户-评分大矩阵后采用协同 过滤进行个性化推荐的方法。以场景 1 为例,对于该网站的用户-电影和用户-书籍评分信息 来说,用户这一维是共通的,即都是该网站的用户。 于是可以直接在用户-电影矩阵后拼接用户-书籍矩阵,新的用户-物品矩阵大小为푢 × (푚 + 푏),物品维的长度是电影数与书籍数的和。该跨领域推荐算法相当于把跨领域问题转 化为单领域问题。因此原先在单领域上的协同过滤推荐方法都可以使用。 2.2.2 基于联合矩阵分解的协同过滤 把跨领域问题转化成单领域问题的方法的优势在于简单易懂,且可直接将现有的单领域协同 过滤算法进行移植。当然问题也很明显,这种方法抹平了领域间的差异,用这种方式进行跨 领域推荐效果未必会优于直接对单领域进行推荐,甚至可能会降低推荐效果。 于是,Singh 等人提出了联合矩阵分解(Collective MF)方法。联合矩阵分解(Collective Matrix Factorization, CMF)是基于单矩阵分解改进的算法。联合矩阵分解,需要分解一系列相互关 联的矩阵。它也需要最小化损失函数,这个损失函数的构造是按权重把各相关矩阵的损失函 数相加。其中,所有相关矩阵权重和为 1,且每一个维只能有一个特征矩阵。CMF 在大部分 数据集上的效果会比矩阵合并后分解的效果好,但 CMF 需要训练的参数较多,计算梯度也 需要耗费较多的时间。 在 CMF 的基础上,Jamali 等人提出了异构矩阵分解方法(HeteroMF)。这种方法是联合矩阵 分解(CMF)算法的拓展。异构矩阵是基于上下文的矩阵分解,即对不同的领域有特定的与 该领域相关的参数去调整训练潜在特征,相当于在 CMF 上增加了领域权重参数。通过引入 这些参数,可以提升训练出的特征的准确率。当然,引入这些参数会使得模型变得更复杂, 需要消耗更多的时间来训练模型。 2.2 基于迁移学习的跨领域推荐 人类具有知识迁移的能力,当人学会一项本领后,再去学习另外一项相关的本领会触类旁通。 迁移学习的目标,是利用其他环境学到的知识,来帮助新环境中的学习任务。由此可知,迁 携程技术沙龙个性化推荐专场 426 移学习这个概念的提出,主要就是为跨领域推荐服务的。 研究者提出了用基于迁移学习的跨领域推荐来解决主领域数据稀疏性问题。特别是其中引入 了“密码书(Codebook)”的概念,即用户-物品领域的评分矩阵是有其特性的,用户和用 户,物品和物品可以进行聚类,类与类之间有着较大的差异,而类内部的成员则较为相似。 Codebook 就是一个聚类后的“浓缩矩阵”,对于场景 1,用户-电影评分矩阵是一个푢 × 푚 的矩阵,用户可以聚成 k 类(푘 << 푢),电影可以聚成 l 类(푙 << 푚),那么 Codebook 矩阵就 是一个푘 × 푙的矩阵。这种 Codebook 矩阵,对于不同领域来说是可迁移的。用户-书本领域 通过聚类得到的 Codebook 矩阵,可以直接迁移到用户-电影领域,即可认为两个领域的 Codebook 矩阵是相同的。基于这样的 Codebook 迁移性,文中提出了如下跨领域的协同过 滤方法。 基于迁移学习的跨领域推荐方法有个不能回避的问题,就是 Codebook 的迁移缺乏直接依 据,这种强硬的“迁移”对于某些强相关领域可能有好的效果,但换做一些相关性不是那么 强的领域可能就没有效果甚至是反作用了。 2.3 基于知识的跨领域推荐 基于协同过滤以及基于迁移学习的跨领域推荐方法,都是在评分矩阵上做文章,即通过已知 的主领域评分矩阵以及辅助领域矩阵,填充主领域评分矩阵中的未知区域,根据评分矩阵的 值进行推荐。 然而,跨领域推荐还可以应用在另一种场景中。假设场景 2 如下:有一个音乐导游系统,会 在用户到达特定位置时向用户推荐一系列音乐。这是一种基于用户兴趣点(Point of Interest, POI)的音乐推荐,系统把两个不同领域(地理和音乐)联系起来,通过已知的地理信息向 用户推荐音乐。 与前文不同,此时并没有目标领域(用户-音乐)的偏好信息,是完全利用辅助领域(用户 -地理位置信息)进行推荐。在这种场景下,对不同用户,只要他们所在地理位置相同,都 给他们推荐相同的音乐。这种不依赖用户需要和偏好基础的推荐,被称为基于知识的推荐 (Knowledge-based Recommendation)。在某种程度上,可以看成是一种推理(Inference) 技术。而基于知识的跨领域推荐,顾名思义,是通过学习其他领域的知识,进行主领域推荐。 三、两种新颖的跨领域推荐方法 我们团队(上海交通大学 CIT 实验室)针对跨领域推荐的需要,提出了两种新颖的推荐方法。 3.1 基于张量分解的协同过滤 我们认为在跨领域协同过滤问题中,“领域”的特征需要被明确考虑,所以形成了一个三元 关系:,而矩阵分解模型只能表示两维的关系:。为了学 习这样三元关系上的特征,一个直观的方法就是使用张量分解(TensorFactorization,TF) 模型。然而标准的表示三元关系的张量应该是一个立方体,但是每个领域中物品集的大小是 携程技术沙龙个性化推荐专场 427 不一样的,也就是说每个领域形成的切片大小是不一样的,所以无法形成一个标准的张量。 受 张 量 分 解 模 型 PARAFAC2 启发,我们提出了跨领域三元分解( Cross-Domain TriadicFactorization,CDTF)模型,它是一种非规则的张量分解模型,允许每个领域中物品 的数量不相同。 CDTF 把每个领域划分为一片,允许每个领域中的物品都有自己独立的特征表示,用户潜在 特征矩阵 U 用来表示从多个领域上学习来的共同偏好,领域特征矩阵 D 用来表示每个领域 各自的特性。因此,每一个评分可以视作为 的潜在特征共同作用的结 果。 此外,用户特征和领域特征的相遇作用,生成了领域特定的用户特征:풖푘푖 = 풖푖.∗ 풅푘,其中, 풖푖是用户푖的特征,풅푘是领域푘的特征,퐮푘푖是用户푖在领域푘所表现出的领域特定的用户特征。 因为各个领域的特征풅푘总是可以从其他用户在这个领域的反馈数据中学习得出,所以这样 的三元关系张量模型总是可以得到用户在特定领域的偏好特征,从而避免了盲目迁移的问 题。 从辅助领域传递多少信息到目标领域对结果有很大的影响,CDTF 使用一组权重变量来调节 每个辅助领域对目标领域的影响。我们使用遗传算法设计了一个权重搜寻算法,以此自动搜 寻最优化的权重分配。 在真实数据集上进行的一系列实验证明了 CDTF 的性能要远好于其他比较的主流方法。 3.2 基于双线性多水平模型的跨领域推荐系统 许多物品在上市之前,已经通过各种手段进行了宣传,如:电视广告、在线社交媒体、大众 口碑等。也就是说,在推荐系统中每个用户对一个物品的选择和评价都或多或少地受到外部 信息的影响,所以我们提出了多水平模型来建模复合的信息,以更好的表示出这种多层次的 特征。 图 1 用户的评分是受到多层次因素影响产生的 让我们结合图 1 所示的一个例子来更直观地阐述这个假设。 首先,我们不需要任何用户的反馈信息,仅根据市场因素我们就能估计一个物品的大致的评 携程技术沙龙个性化推荐专场 428 分(记作 r);比如,我们在 iPhone 7 上市之前,就能大致预测它在市场中的评分。 此外,用户所处的群体也对用户的评分有着直接的影响;比如,时尚圈的人士可能给某个奢 侈品比市场评分更高的分数(正偏倚)而 IT 工程师则可能给这个奢侈品较低的分数(负偏 倚)。 对于这样的群体偏倚,我们记作푏푐,它反应了整个群体的偏好。因为每个人的背景、收入、 性格都不同,所以一个群体中的用户也存在着一定差异,对 这些由个人差异引起的评分偏倚, 我们记作푏푖。结合上述这些因素的影响,可以把一个用户在一个物品上的评分表示为푅 = 푟 + 푏푐 + 푏푖,也就是说这个评分是有多层次的因素综合决定的,而不仅仅是个人因素。 基于这样的多层次因素的假设,在处理冷启动用户时,我们仍然可以用高层因素产生的评分 푟 + 푏푐去产生一个大概的预测,即使用户的个人数据不存在。由此可见,我们所提出的基于 多层次因素假设的模型要比传统的仅基于用户个人因素的模型更为健壮。 为了建模上述的多层次因素,我们引入了多水平模型。进一步,我们建立的基于潜在因素的 多水平模型中,涉及到了多层次的用户因素和物品因素,并使用类似于矩阵分解模型的双线 性形式来建模用户因素和物品因素的相互作用,因此称之为双线性多水平分析(Bi-Linear Multilevel Analysis,BLMA)模型。BLMA 能对跨领域协同过滤中的多层次因素进行建模,以 此来解决冷启动和盲目迁移等问题。同时,我们设计了一个并行的 Gibbs 采样算法来更为高 效地学习参数。通过在真实数据集上进行的一系列实验证明了 BLMA 的性能要好于其他进 行比较的主流方法。 四、展望 事实上,领域概念可以表示成任何可以区分的场景。例如就用户购买图书而言,五年前你的 书籍购买和目前你的书籍购买就属于不同的领域,你在京东上买书和在当当上买书也是不同 的领域,你买财经类的书和买科技类的书也是不同的领域。因此,跨领域推荐具有广泛的应 用场景。 目前,推荐应用的场景越来越广泛,如网页排名(Google)、新闻内容展示(Yahoo)、垃圾 邮件过滤(Google)、在线约会(OK Cupid)、个性化广告显示(Yahoo)等。显然,推荐系 统已经成为了信息检索、数据挖掘、社会网络分析、计算广告学等众多领域的核心技术之一。 随着移动穿戴设备、智能家居的发展,推荐系统也必将融入其中,与医疗、营养、食品等领 域相结合,提供诸如实时健康建议、疾病预处理、智能营养配餐等与人们生活休戚相关的关 键服务。而在这些场合中,跨领域推荐的需求将变得更为显著。 备注:本文作者为曹健、欧辉思和胡亮(主要内容取材于曹健指导的硕士生欧辉思和博士生 胡亮的学位论文,并经过了曹健的修改)。欢迎有兴趣探讨和应用跨领域推荐算法的单位和 研究者和作者联系,曹健 cao-jian@sjtu.edu.cn。 携程技术沙龙前端专场 429 携程技术沙龙前端专场 携程技术沙龙前端专场 430 IMVC(同构 MVC)的前端实践 [作者简介]古映杰,携程度假研发部前端和 node.js 架构负责人。开源库 react-lite 作者。 现场视频:https://v.qq.com/x/page/y0385edyr66.html 随着 Backbone 等老牌框架的逐渐衰退,前端 MVC 发展缓慢,有逐渐被 MVVM/Flux 所 取代的趋势。 然而,纵观近几年的发展,可以发现一点,React/Vue 和 Redux/Vuex 是分别在 MVC 中的 View 层和 Model 层做了进一步发展。如果 MVC 中的 Controller 层也推进一步,将得到 一种升级版的 MVC,我们称之为 IMVC(同构 MVC)。 IMVC 可以实现一份代码在服务端和浏览器端皆可运行,具备单页应用和多页应用的所有优 势,并且可以这两种模式里通过配置项进行自由切换。配合 Node.js、Webpack、Babel 等 基础设施,我们可以得到相比之前更加完善的一种前端架构。 一、同构的概念和意义 1.1、isomorphic 是什么? isomorphic,读作[ˌaɪsə'mɔ:fɪk],意思是:同形的,同构的。 维基百科对它的描述是:同构是在数学对象之间定义的一类映射,它能揭示出在这些对象的 属性或者操作之间存在的关系。若两个数学结构之间存在同构映射,那么这两个结构叫做是 同构的。一般来说,如果忽略掉同构的对象的属性或操作的具体定义,单从结构上讲,同构 的对象是完全等价的。 同构,也被化用在物理、化学以及计算机等其他领域。 1.2、isomorphicjavascript isomorphicjavascript(同构 js),是指一份 js 代码,既然可以跑在浏览器端,也可以跑在服 务端。 携程技术沙龙前端专场 431 图 1 同构 js 的发展历史,比 progressive web app 还要早很多。2009 年, node.js 问世,给予 我们前后端统一语言的想象;更进一步的,前后端公用一套代码,也不是不可能。 有一个网站 isomorphic.net,专门收集跟同构 js 相关的文章和项目。从里面的文章列表来 看,早在 2011 年的时候,业界已经开始探讨同构 js,并认为这将是未来的趋势。 可惜的是,同构 js 其实并没有得到真正意义上的发展。因为,在 2011 年,node.js 和 ECMAScript 都不够成熟,我们并没有很好的基础设施,去满足同构的目标。 现在是 2017 年,情况已经有所不同。ECMAScript 2015 标准定案,提供了一个标准的模块 规范,前后端通用。尽管目前 node.js 和浏览器都没有实现 ES2015 模块标准,但是我们有 Babel 和 Webpack 等工具,可以提前享用新的语言特性带来的便利。 二、同构的种类和层次 2.1、同构的种类 同构 js 有两个种类:「内容同构」和「形式同构」。 其中,「内容同构」指服务端和浏览器端执行的代码完全等价。比如: 携程技术沙龙前端专场 432 functionadd(a, b) { return a + b } 不管在服务端还是浏览器端,add 函数都是一样的。 而「形式同构」则不同,从原教旨主义的角度上看,它不是同构。因为,在浏览器端有一部 分代码永远不会执行,而在服务端另一部分代码永远不会执行。 比如: functiondoSomething() { if (isServer) { // do something in server-side } else if (isClient) { // do something in client-side } } 在 npm 里,有很多 package 标榜自己是同构的,用的方式就是「形式同构」。如果不作特 殊处理,「形式同构」可能会增加浏览器端加载的 js 代码的体积。比如 React,它的 140+kb 的体积,是把只在服务端运行的代码也包含了进去。 2.2、同构的层次 同构不是一个布尔值,true 或者 false;同构是一个光谱形态,可以在很小范围里上实现同 构,也可以在很大范围里实现同构。  function 层次:零碎的代码片断或者函数,支持同构。比如浏览器端和服务端都实现了 setTimeout 函数,比如 lodash/underscore 的工具函数都是同构的。  feature 层次:在这个层次里的同构代码,通常会承担一定的业务职能。比如 React 和 Vue 都借助 virtual-dom 实现了同构,它们是服务于 View 层的渲染;比如 Redux 和 Vuex 也是同构的,它们负责 Model 层的数据处理。  framework 层次:在框架层面实现同构,它可能包含了所有层次的同构,需要精心处理 支持同构和不支持同构的两个部分,如何妥善地整合在一起。 我们今天所讨论的 isomorphic-mvc(简称 IMVC),是在 framework 层次上实现同构。 三、同构的价值和作用 3.1、同构的价值 同构 js,不仅仅有抽象上的美感,它还有很多实用价值。 携程技术沙龙前端专场 433  SEO 友好:View 层在浏览器端和服务端都可以运行,意味着可以在服务端吐出 html, 支持搜索引擎的抓取。  加快访问体验:服务端渲染可以加快浏览器端的首次访问的渲染速度,而浏览器端渲染, 可以加快用户交互时的反馈速度。  代码的可维护性:同构可以减少语言切换的成本,减小代码的重复率,增加代码的可维 护性。 不使用同构方案,也可以用别的办法实现前两个的目标,但是别的办法却难以同时满足三个 目标。 3.2、同构如何加快访问体验 纯浏览器端渲染的问题在于,页面需要等待 js 加载完毕之后,才可见。 图 2 client-side renderging 服务端渲染可以加速首次访问的体验,在 js 加载之前,页面就渲染了首屏。但是,用户只 对首次加载有耐心,如果操作过程中,频繁刷新页面,也会带给用户缓慢的感觉。 携程技术沙龙前端专场 434 图 3 server-side renderging 同构渲染则可以得到两种好处,在首次加载时用服务端渲染,在交互过程中则采取浏览器端 渲染。 3.3、同构是未来的趋势 从历史发展的角度看,同构确实是未来的一大趋势。 在 Web 开发的早期,采用的开发模式是:fat-server, thin-client 携程技术沙龙前端专场 435 图 4 前端只是薄薄的一层,负责一些表单验证,DOM 操作和 JS 动画。在这个阶段,没有「前 端工程师」这个工种,服务端开发顺便就把前端代码给写了。 在 Ajax 被发掘出来之后,Web 进入 2.0 时代,我们普遍推崇的模式是:thin-server, fat- client 携程技术沙龙前端专场 436 图 5 越来越多的业务逻辑,从服务端迁移到前端。开始有「前后端分离」的做法,前端希望服务 端只提供 restful 接口和数据持久化。 但是在这个阶段,做得不够彻底。前端并没有完全掌控渲染层,起码 html 骨架需要服务端 渲染,以及前端实现不了服务端渲染。 为了解决上述问题,我们正在进入下一个阶段,这个阶段所采取的模式是:shared, fat-server, fat-client。 携程技术沙龙前端专场 437 图 6 通过 node.js 运行时,前端完全掌控渲染层,并且实现渲染层的同构。既不牺牲服务端渲染 的价值,也不放弃浏览器端渲染的便利。 这就是未来的趋势。 四、同构的实现策略 要实现同构,首先要正视一点,全盘同构是没有意义的。为什么? 服务端和浏览器端毕竟是两个不同的平台和环境,它们专注于解决不同的问题,有自身的特 点,全盘同构就抹杀了它们固有的差异,也就无法发挥它们各自的优势。 因而,我们只会在 client 和 server 有交集的部分实现同构。就是在服务端渲染 html 和在 浏览器端复用 html 的整个过程里,实现同构。 我们采取的主要做法有两个:1)能够同构的代码,直接复用;2)无法同构的代码,封装成 形式同构。 举几个例子。 获取 User-Agent 字符串。 携程技术沙龙前端专场 438 图 7 我们可以在服务端用 req.get('user-agent') 模拟出 navigator 全局对象,也可以提供一个 getUserAgent 的方法或函数。 获取 Cookies。 携程技术沙龙前端专场 439 图 8 Cookies 处理在我们的场景里,存在快捷通道,因为我们只专注首次渲染的同构,其它的操 作可以放在浏览器端二次渲染的时候再处理。 Cookies 的主要用途发生在 ajax 请求的时候,在浏览器端 ajax 请求可以设置为自动带上 Cookies,所以只需要在服务端默默地在每个 ajax 请求头里补上 Cookies 即可。 Redirects 重定向处理 携程技术沙龙前端专场 440 图 9 重定向的场景比较复杂,起码有三种情况: 服务端 302 重定向: res.redirect(xxx) 浏览器端 location 重定向:location.href = xxx 和 location.replace(xxx) 浏览器端 pushState 重定向:history.push(xxx) 和 history.replace(xxx) 我们需要封装一个 redirect 函数,根据输入的 url 和环境信息,选择正确的重定向方式。 五、IMVC 架构 5.1、IMVC 的目标 IMVC 的目标是框架层面的同构,我们要求它必须实现以下功能 用法简单,初学者也能快速上手 只维护一套 ES2015+ 的代码 既是单页应用,又是多页应用(SPA + SSR) 可以部署到任意发布路径(Basename/RootPath) 一条命令启动完备的开发环境 一条命令完成打包/部署过程 携程技术沙龙前端专场 441 有些功能属于运行时的,有些功能则只服务于开发环境。JavaScript 虽然是一门解释型语言, 但前端行业发展到现阶段,它的开发模式已经变得非常丰富,既可以用最朴素的方式,一个 记事本加上一个浏览器,也可以用一个 IDE 加上一系列开发、测试和部署流程的支持。 5.2、IMVC 的技术选型 Router:create-app = history + path-to-regexp View:React = renderToDOM || renderToString Model:relite = redux-like library Ajax:isomorphic-fetch 理论上,IMVC 是一种架构思路,它并不限定我们使用哪些技术栈。不过,要使 IMVC 落地, 总得做出选择。上面就是我们当前选择的技术栈,将来它们可能升级或者替换为其它技术。 5.3、为什么不直接用 React 全家桶? 大家可能注意到,我们使用了许多 React 相关的技术,但却不是所谓的 React 全家桶,原 因如下: 目前的 React 全家桶其实是野生的,Facebook 并不用 React-Router 的理念难以满足要求 Redux 适用于大型应用,而我们的主要场景是中小型 升级频繁导致学习成本过高,需封装一层更简洁的 API 目前的全家桶,只是社区里的一些热门库的组合罢了。Facebook 真正用的全家桶是 react|flux|relay|graphql,甚至他们并不用 React 做服务端渲染,用的是 PHP。 我们认为 React-Router 的理念在同构上是错误的。它忽视了一个重大事实:服务端是 Router 路由驱动的,把 Router 和作为 View 的 React 捆绑起来,View 已经实例化了, Router 怎么再加载 Controller 或者异步请求数据呢? 从函数式编程的角度看,React 推崇纯组件,需要隔离副作用,而 Router 则是副作用来源, 将两者混合在一起,是一种污染。另外,Router 并不是 UI,却被写成 JSX 组件的形式,这 也是有待商榷的。 所以,即便是当前最新版的 React-Router-v4,实现同构渲染时,做法也复杂而臃肿,服务 端和浏览器端各有一个路由表和发 ajax 请求的逻辑。 至于 Redux,其作者也已在公开场合表示:「你可能不需要 Redux」。在引入 redux 时,我 们得先反思一下引入的必要性。 毫无疑问,Redux 的模式是优秀的,结构清晰,易维护。然而同时它也是繁琐的,实现一个 功能,你可能得跨文件夹地操作数个文件,才能完成。这些代价所带来的显著好处,要在 app 复杂到一定程度时,才能真正体会。其它模式里,app 复杂到一定程度后,就难以维护了; 携程技术沙龙前端专场 442 而 Redux 的可维护性还依然坚挺,这就是其价值所在。(值得一提的是,基于 redux 再封 装一层简化的 API,我认为这很可能是错误的做法。Redux 的源码很简洁,意图也很明确, 要简化固然也是可以的,但它为什么自己不去做?它是不是刻意这样设计呢?你的封装是否 损害了它的设计目的呢?) 在使用 Redux 之前要考虑的是,我们 web-app 属于大型应用的范畴吗? 前端领域日新月异,框架和库的频繁升级让开发者应接不暇。我们需要根据自身的需求,进 行二次封装,得到一组更简洁的 API,将部分复杂度隐藏起来,以降低学习成本。 5.4、用 create-app 代替 react-router create-app 是我们为了同构而实现的一个 library,它由下面三部分组成: history: react-router 依赖的底层库 path-to-regexp: expressjs 依赖的底层库 Controller:在 View(React) 层和 Model 层之外实现 Controller 层 create-app 复用 React-Router 的依赖 history.js,用以在浏览器端管理 history 状态;复用 expressjs 的 path-to-regexp,用以从 path pattern 中解析参数。 我们认为,React 和 Redux 分别对应 MVC 的 View 和 Model,它们都是同构的,我们需 要的是实现 Controller 层的同构。 5.4.1、create-app 的同构理念 图 10 create-app 实现同构的方式是: 输入 url,router 根据 url 的格式,匹配出对应的 controller 模块 携程技术沙龙前端专场 443 调用 module-loader 加载 controller 模块,拿到 Controller 类 View 和 Model 从属于 Controller 类的属性 newController(location, context) 得到 controller 实例 调用 controller.init 方法,该方法必须返回 view 的实例 调用 view-engine 将 view 的实例根据环境渲染成 html 或者 dom 或者 native-ui 等 上述过程在服务端和浏览器端都保持一致。 5.4.2、create-app 的配置理念 服务端和浏览器端加载模块的方式不同,服务端是同步加载,而浏览器端则是异步加载;它 们的 view-engine 也是不同的。如何处理这些不一致? 答案是配置。 constapp = createApp({ type: 'createHistory', container: '#root', context: { isClient: true|false, isServer: false|true, ...injectFeatures }, loader: webpackLoader|commonjsLoader, routes: routes, viewEngine: ReactDOM|ReactDOMServer, }) app.start()|| app.render(url, context) 服务端和浏览器端分别有自己的入口文件:client-entry.js 和 server.entry.js。我们只需提供 不同的配置即可。 在服务端,加载 controller 模块的方式是 commonjsLoader;在浏览器端,加载 controller 模块的方式则为 webpackLoader。 在服务端和浏览器端,view-engine 也被配置为不同的 ReactDOM 和 ReactDOMServer。 每个 controller 实例,都有 context 参数,它也是来自配置。通过这种方式,我们可以在运 行时注入不同的平台特性。这样既分割了代码,又实现了形式同构。 5.4.3、create-app 的服务端渲染 我们认为,简洁的,才是正确的。create-app 实现服务端渲染的代码如下: 携程技术沙龙前端专场 444 constapp = createApp(serverSettings) router.get('*',async (req, res, next) => { try { const { content } = awaitapp.render(req.url, serverContext) res.render('layout', { content }) } catch(error) { next(error) } }) 没有多余的信息,也没有多余的代码,输入一个 url 和 context,返回具有真实数据 html 字符串。 5.4.4、create-app 的扁平化路由理念 React-Router 支持并鼓励嵌套路由,其价值存疑。它增加了代码的阅读成本,以及各个路由 模块之间的关系与 UI(React 组件)的嵌套耦合在一起,并不灵活。 使用扁平化路由,可以使代码解耦,容易阅读,并且更为灵活。因为,UI 之间的复用,可 以通过 React 组件的直接嵌套来实现。 基于路由嵌套关系来复用 UI,容易遇上一个尴尬场景:恰好只有一个页面不需要共享头部, 而头部却不在它的控制范畴内。 //routes exportdefault [{ path: '/demo', controller: require('./home/controller') }, { path: '/demo/list', controller: require('./list/controller') }, { path: '/demo/detail', controller: require('./detail/controller') }] 如你所见,我们的 path 对应的并不是 component,而是 controller。通过新增 controller 层,我们可以实现在 view 层的 component 实例化之前,就借助 controller 获取首屏数据。 next.js 也是一个同构框架,它本质上是简化版的 IMVC,只不过它的 C 层非常薄,以至于 直接挂在 View 组件的静态方法里。它的路由配置目前是基于 View 的文件名,其 Controller 层是 View.getInitialProps 静态方法,只服务于获取初始化 props。 这一层太薄了,它其实可以更为丰富,比如提供 fetch 方法,内置环境判断,支持 jsonp, 携程技术沙龙前端专场 445 支持 mock 数据,支持超时处理等特性,比如自动绑定 store 到 view,比如提供更为丰富 的生命周期 pageWillLeave(页面将跳转到其他路径)和 windowWillUnload (窗口即将关 闭)等。 总而言之,副作用不可能被消灭,只能被隔离,如今 View 和 Model 都是 pure-function 和 immutabel-data 的无副作用模式,总得有角色承担处理副作用的职能。新的抽象层 Controller 应运而生。 5.4.5、create-app 的目录结构 ├── src // 源代码目录 │ ├── app-demo // demo 目录 │ ├── app-abcd // 项目 abcd 平台目录 │ │ ├── components // 项目共享组件 │ │ ├── shared // 项目共享方法 │ │ └── BaseController// 继承基类 Controller 的项目层 Controller │ │ ├── home // 具体页面 │ │ │ ├── controller.js// 控制器 │ │ │ ├── model.js // 模型 │ │ │ └── view.js // 视图 │ │ ├── * // 其他页面 │ │ └── routes.js // abc 项目扁平化路由 │ ├── app-* // 其他项目 │ ├── components // 全局共享组件 │ ├── shared // 全局共享文件 │ │ └── BaseController // 基类 Controller │ ├── index.js // 全局 js 入口 │ └── routes.js // 全局扁平化路由 ├── static // 源码 build 的目标静态文件夹 如上所示,create-app 推崇的目录结构跟 redux 非常不同。它不是按照抽象的职能 actionCreator|actionType|reducers|middleware|container 来安排的,它是基于 page 页面来 划分的,每个页面都有三个组成部分:controller,model 和 view。 用 routes 路由表,将 page 串起来。 create-app 采取了「整站 SPA」的模式,全局只有一个入口文件,index.js。src 目录下的文 件都所有项目共享的框架层代码,各个项目自身的业务代码则在 app-xxx 的文件夹下。 这种设计的目的是为了降低迁移成本,灵活切分和合并各个项目。  当某个项目处于萌芽阶段,它可以依附在另一个项目的 git 仓库里,使用它现成的基础 设施进行快速开发。  当两个项目足够复杂,值得分割为两个项目时,它们可以分割为两个项目,各自将对方 携程技术沙龙前端专场 446 的文件夹整个删除即可。  当两个项目要合并,将它们放到同一 git 仓库的不同 app-xxx 里即可。  我们使用本地路由表 routes.js 和 nginx 配置协调 url 的访问规则 每个 page 的 controller.js,model.js 和 view.js 以及它们的私有依赖,将会被单独打包到 一个文件,只有匹配 url 成功时,才会按需加载。保证多项目并存不会带来 js 体积的膨胀。 5.5、controller 的基本模式 我们新增了 controller 这个抽象层,它将承担连接 Model,View,History,LocalStorage, Server 等对象的职能。 Controller 被设计为 OOP 编程范式的一个 class,主要目的就是为了让它承受副作用,以便 View 和 Model 层保持函数式的纯粹。 Controller 的基本模式如下: classMyController extends BaseController { requireLogin = true // 是否依赖登陆态,BaseController 里自动处理 View = View // 视图 initialState = { count: 0 } // model 初始状态 initialState actions = actions // model 状态变化的函数集合 actions handleIncre = () => { // 事件处理器,自动收集起来,传递给 View 组件 let { history, store, fetch, location,context } = this // 功能分层 let { INCREMENT } = store.actions INCREMENT() // 调用 action,更新 state, view 随之自动更新 } async shouldComponentCreate() {} // 在这里鉴权,return false async componentWillCreate() {} // 在这里 fetch 首屏数据 componentDidMount() {} // 在这里 fetch 非首屏数据 pageWillLeave() {} // 在这里执行路由跳转离开前的逻辑 windowWillUnload() {} // 在这里执行页面关闭前的逻辑 } 我们将所有职能对象放到了 controller 的属性中,开发者只需提供相应的配置和定义,在丰 富的生命周期里按需调用相关方法即可。 它的结构和模式跟 vue 和微信小程序有点相似。 5.6、redux 的简化版 relite 尽管作为中小型应用的架构,我们不使用 Redux,但是对于 Redux 中的优秀理念,还是可 以吸收进来。 携程技术沙龙前端专场 447 所以,我们实现了一个简化版的 redux,叫做 relite。  actionType,actionCreator, reducer 合并  自动 bindActionCreators,内置异步 action 的支持 letEXEC_BY = (state, input) => { let value = parseFloat(input, 10) return isNaN(value) ? state : { ...state, count: state.count + value } } letEXEC_ASYNC = async (state, input) => { await delay(1000) return EXEC_BY(state, input) } letstore = createStore( { EXEC_BY, EXEC_ASYNC }, { count: 0 } ) 我们希望得到的是 redux 的两个核心:1)pure-function,2)immutable-data。 所以 action 函数被设计为纯函数,它的函数名就是 redux 的 action-type,它的函数体就 是 redux 的 reducer,它的第一个参数是当前的 state,它的第二个参数是 redux 的 actionCreator 携带的数据。并且,relite 内置了 redux-promise 和 redux-thunk 的功能, 开发者可以使用 async/await 语法,实现异步 action。 relite 也要求 state 尽可能是 immutable,并且可以通过额外的 recorder 插件,实现 time- travel 的功能。 5.7、Isomorphic-MVC 的工程化设施 上面讲述了 IMVC 在运行时里的一些功能和特点,下面简单地描述一下 IMVC 的工程化设 施。我们采用了:  node.js 运行时,npm 包管理  expressjs 服务端框架  babel 编译 ES2015+ 代码到 ES5  webpack 打包和压缩源码  standard.js 检查代码规范  prettier.js+ git-hook 代码自动美化排版  mocha 单元测试 携程技术沙龙前端专场 448 5.7.1、如何实现代码实时热更新? 目标:一个命令启动开发环境,修改代码不需重启进程 做法:一个 webpack 服务于 client,另一个 webpack 服务于 server client:express + webpack-dev-middleware 在内存里编译 server:memory-fs + webpack + vm-module 服务端的 webpack 编译到内存模拟的文件系统,再用 node.js 内置的虚拟机模块执行后得 到新的模块 5.7.2、如何处理 CSS 按需加载? 问题根源:浏览器只在 dom-ready 之前会等待 css 资源加载后再渲染页面 问题描述:当单页跳转到另一个 url,css 资源还没加载完,页面显示成混乱布局 处理办法:将 css 视为预加载的 ajax 数据,以 style 标签的形式按需引入 优化策略:用 context 缓存预加载数据,避免重复加载 5.7.3、如何实现代码切割、按需加载?  不使用 webpack-only 的语法 require.ensure  在浏览器里 require 被编译为加载函数,异步加载  在 node.js 里 require 是同步加载 //webpack.config.js { test: /controller\.jsx?$/, loader: 'bundle-loader', query: { lazy: true, name: '[1]-[folder]', regExp:/[\/\\]app-([^\/\\]+)[\/\\]/.source }, exclude: /node_modules/ } 5.7.4、如何处理静态资源的版本管理?  以代码的 hash 为文件名,增量发布 携程技术沙龙前端专场 449  用 webpack.stats.plugin.js 生成静态资源表  express 使用 stats.json 的数据渲染页面 //webpack.config.js output= { path: outputPath, filename: '[name]-[hash:6].js', chunkFilename: '[name]-[chunkhash:6].js' } 5.7.5、如何管理命令行任务?  使用 npm-scripts 在 package.json 里完成 git、webpack、test、prettier 等任务的串并 联逻辑  npmstart 启动完整的开发环境  npmrun start:client 启动不带服务端渲染的开发环境  npmrun build 启动自动化编译,构建与压缩部署的任务  npmrun build:show-prod 用 webpack-bundle-analyzer 可视化查看编译结果 六、结语 IMVC 经过实践和摸索,已被证明是一种有效的模式,它以较高的完成度实现了真正意义上 的同构。不再局限于纸面上的理念描述,而是一个可以落地的方案,并且实际地提升了开发 体验和效率。后续我们将继续往这个方向探索。 携程技术沙龙前端专场 450 Qreact,去哪儿网的迷你 react 方案 [作者简介]钟钦成,网名司徒正美,著名的 JavaScript 专家,去哪儿网前端架构师。在 GITHUB 拥有复数个著名的轮子,著有《javascript 框架设计》一书。 现场视频:https://v.qq.com/x/page/g0385p9io8o.html 去哪儿网在 React Native 深耕多年,对 React 内部实现的了解在国内应该是非常领先的。迫 于项目对 React 体积的极致需求,我们推出了自己的迷你化方案——Qreact。 Qreact 比市面上的其他迷你 react 框架的实用性更强,基本上可以说无缝切换到任何已有的 react 15 工程中,能极大地改善对体积的压力,这在对流量非常苛刻的移动端上尤其重要! 我们从今年 1 月份快速启动项目,在 1 个月内大致完成了功能,Demo,并配合现有的复杂 例子进行验收。本文将分享我们在做这轮子过程中的一些想法,包括竞品分析,实现思路, 项目风险控制等等。 一、核心需求 我们并不是无事找事,为造轮子而造轮子。虽然有 KPI 的成分,但它的核心需求是来自业务 线,可以说就算我们不造,公司内部其他人也会造一个,但那个质量可能无法保证,毕竟公 司绝大部分的高手都分配到我们事业部。 由于没有产品经理,我们需要充当产品经理的角色,聆听业务线的需求,自己挖掘需求。 去哪儿深耕 React 多年,构建了两个基于 React 的 UI 库,它们都是用于移动端。如果这些 库都是内置在 APP 中,应该没有要求。但是去哪儿分成十来个事业部,根据事业部的赚钱 能力分配更新包的体积。为了能让用户在 wifi 上更新我们的 APP,更新包的体积一般不超过 100MB,因此像这样公用的框架与库体积越少越好。 此外,其中一个 UI 库是用于手机浏览器上,我们称之为 React Web,用户每次打开我们的 页面,都会加载一遍 React 与相关组件,这个对体积就更加敏感。因此当我们完成 React Web, 就着眼于迷你 React 的开发。 这个新的框架有三个核心需求: 1、体积小。移动端对体积一向敏感,因此在 jQuery 时代,zepto 能割踞一方。 2、支持事件系统,这不是简单 add Event Listener,是 React 原来的那套 Synthetic Event。 它帮我们搞定 300ms 延迟,还有滚动列表时误触发点击的问题。如果你不用它,你需要让 业务线参照 iscroll 的原理自造一个。 携程技术沙龙前端专场 451 3、能直接替换。换言之,新框架与原框架的功能几乎一致。因此许多业务已经 用 React 开发完毕,不希望做太多改动。由于业务线有时时间赶,碰到难题搞不定,会倾向 用一些怪招歪招。在无法预料对方用什么 API 的情况,新框架框架覆盖原 React 的各种偏门 用法。 下面是一个紧急修复的补丁: 我们列举一下各种偏门的 API 与用法 1、mixin 包含 mixin。这个在 RN 很常见。 2、ref, setState 传函数的用法 3、context 与 getChildContext 的运用,虽然官方明确不建议大家用,但是著名的 react-redux 在源码里用到了。 4、_rootNodeID, _hostParent,_hostNode 这些内部属性用在后端渲染与事件系统中。 二、竞品分析 携程技术沙龙前端专场 452 在立项后,我们开始找市场上的同类产品,如果有满足的,我们就不用开发了。目前,前端 要找这些框架,只有一个去处,就是 GITHUB。这是开源界的宝库,应有尽有,琳琅满目。 由于代码公开,大家可以抄抄,因此每流行一样的东西,大家都是一窝蜂上的。除开那些纯 练手的项目,每个库都有自己独到之处。 自从 React 推出虚拟 DOM 来解决复杂应用的性能问题以来,GITHUB 上有上百个虚拟 DOM 的库,包括之前的 angular, vue2 都在底层使用这种性能利器。 这是一些虚拟 DOM 框架或库的数据,从相似度,性能,流行度,版本更新等情况综合考虑, 携程技术沙龙前端专场 453 我们也只能选上面三者:inferno, preact, react-lite。 inferno 从各方面来看,是无可挑剔的,性能比排行第二的 kivi 快 20 倍,更不用说 vue, angular 什么之流。每个库都会吹自己的框架有多快,但 inferno 的主页上有大量测试页面, 是有真实数据支撑的。但是它偏面追求性能,源码里的可读性太差。看不懂,无从入手,只 能遗憾地放弃了。 其他性能流有 citijs, snabbdom, virtual-dom。最早搞出性能引擎的是 citijs,然后基于它上面 分化出 kivi, ivi, snabbdom,然后 vue2.0 又直接将 snabbdom 库整合到它里面。virtual-dom 则是走另一种性能优化方式。但它们都是迷你库,API 与 React 差太远。 于是只剩下 preact 与 react-lite。 三、设计思路 由于是业务线的迫切需求,并且拖得越久,就越多项目用上 RN,到时需要回归测试的项目 就越多,因此必须尽快搞出来。我们就不打算重造轮子,而是在已有轮子上改改。 第一版是基于 react-lite。这是因为 react-lite 是携程的工业聚大神写的,携程是我们的兄弟 公司,应该比较好交流。但现实中发现,这个库的扩展性不足,比如说事件系统那里,需要 传入 4 个参数,在 react-lite 里只能拿到三个参数,想尽方法也无法凑齐第四参数。还有一 些内部属性,渲染流程与原装 React 差得太远了。在双方折腾了 2 个星期后,我们组有人心 灰意冷,着手后备方案,preact。 preact 比起 react-lite 多出几个优势: 1、官方提供兼容补丁 preact-compat 2、插件巨多 3、ISSUR 活跃,当天提问题,大概到晚上,外国人起床就有回应了。 4、扩展方便 尤其第 4 点,在开发 qreact 时,我们都为双方提了不少 ISSUE。其实程序员还是比较腼腆, 不愿麻烦人,因此我们写框架时还是多留一些扩展接口吧。 整个 qreact 的架构大概就是: qreact= preact 改+preact-compat 改+react-web 事件系统迷你版 在 preact 的源码里一个叫 options.js 的文件,里面有一个 options 的对象,它会被框架的多 个关键方法调用。我们通过为它重新实现某些方法,就达到改写框架的目标。 https://github.com/developit/preact/blob/master/src/options.js 携程技术沙龙前端专场 454 两个 react-lite 的难点问题,由于 options 的扩展机制太灵活了,一下子被摆平。 1、事件系统需要传入 4 个参数的问题。在 options 添加一个 handle Event 方法。 2、内置属性问题,在 options 重写 vnode 方法。 重点说一下内部属性问题: 携程技术沙龙前端专场 455 随着版本的升级,这些内部属性越来越多,这里讲解一下其中三个: 这了让 preact 支持它们,我们是在框架 diff 节点时,重新添加上它们的。因为这时,我们能 轻松知道一个节点在 DOM 树的上下关系。 最后是对事件系统进行瘦身。React 有 16000 行,其中 10000 行都是事件系统相关的。再加 上 React Native 中的 Pan Responder 系统。这体积非常庞大。但是如果我们将要支持的浏览 器收窄一点,不支持 IE 系列与 firefox 系列。起码在事件对象的构造器上,我们可以做一些 合并操作。 下面 React 中的事件构造器列表: https://github.com/facebook/react/tree/v15.3.2/src/renderers/dom/client/syntheticEvents 携程技术沙龙前端专场 456 它们浓缩成一个事件构造器后,代码少了 3000 行。 我们再对事件插件进行围剿。因为我们不需要 mouseenter, mouseleave, input, composition, beforeinput 的兼容,又可以减少许多行。 https://github.com/facebook/react/tree/v15.3.2/src/renderers/dom/client/eventPlugins 最后成果是 qreact 缩少到 6000 行,事件系统占其中的 4000 行,min 后的体积为 39kb。原 版 React 的 min 体积是 140kb。减少近 80kb。 体积算是达标了,那么性能如何呢?毕竟我们使用 React 的初衷是因为它的性能太好了。 React 的性能主要来自它的虚拟 DOM 的 diff 算法。体积缩水了,它的 diff 算法肯定也打折 扣。这时 preact 提出两个后备方案: 1、减少要比较的虚拟 DOM 的数量 hydrate。这是发端于 inferno 的优化方案,通过合并相 邻的字符串或数字,减少虚拟 DOM 数量。 2、减少要生成的真实 DOM 的数据 recycle。上面的 hycycle 也会减少真实 DOM 的数量,但 我们还可以将要移除的真实 DOM 保存起来,重复利用这些真实 DOM。 通过这两种机制,大大弥补 qreact diff 算法的缺憾。此外,我们还可以通过动静分离的方式 来提高性能。在定义 JSX 时,我们就能得知某个元素是否包含花括号,有花括号说明其是动 态的,反之是静态的,但一个元素与其所有子孙都没有花括号,那么这个子树可以整体缓存 起来,以后转换为真实 DOM 后,它能缓存起来。 然后在组件的 render 方法中,对于这部分的 React Element 每次返回相同的对象,并且在上 面添加一个标记,碰到两个对象都有这个标记,就直接返回,不往下比较了。这是 inferno 提出的另一个性能优化方案。 最后验证性能是用 ListView 进行测试的,和原来一样流畅。 四、分享展示 携程技术沙龙前端专场 457 里面最重要的两个例子就是 yo-demo 与 qunar-react-native-web。 携程技术沙龙云海机器学习专场 458 携程技术沙龙云海机器学习专场 携程技术沙龙云海机器学习专场 459 模型优化不得不思考的几个问题 [作者简介]胡淏,美团算法工程师,毕业于哥伦比亚大学。先后在携程、支付宝、美团从事 算法开发工作。了解风控、基因、旅游、即时物流相关问题的行业领先算法方案与流程。 现场视频:https://v.qq.com/x/page/x0397y7cwzp.html 我们平时都在积累自己的“弹药库”:分类、回归、无监督模型,kaggle 上面特征变换的黑 魔法,样本失衡的处理方法,缺失值填充... 大概可以归类成模型和特征两个点。我们在每个 点都已经做得很好,所以我们都拥有一张绿卡,跨过了在数据相关行业发挥模型技术价值的 准入门槛。 在这个时候,比较关键的下一步,就是高效的技术变现能力,所谓高效,就是解决业务核心 问题的专业能力。这篇文章就在描述这些专业能力,也就是模型优化的四个要素:模型、数 据、特征、业务,还有更重要的,他们在模型项目中的优先级。 本文首先综合介绍模型项目的优先级,模型项目推进的四个要素,并按照优先级顺序依次展 开四个要素细节实施过程中需要注意的方方面面。 图 1 一、模型项目推进的四要素 模型优化,离不开这四个要素:模型、数据、特征、业务。 项目推进过程中,四个要素相互之间的优先级大致是:业务 > 特征 > 数据> 模型。 携程技术沙龙云海机器学习专场 460 图 2:四要素解决问题细分 + 优先级 二、业务 一个模型项目有好的技术选型,完备的特征体系,高质量的数据一定是很加分的,不过有个 大前提决定项目的好与坏,就是这个项目的技术目标是否在解决当下核心业务问题。 业务问题包含两个方面,业务 kpi 和 deadline。比如,如果业务问题是:在两周之内降低目 前手机丢失带来的支付宝销赃风险,这时如果你的方案是研发手机丢失的核心特征,比如改 密是否合理,基本上就死的很惨,因为两周根本完不成,改密合理性也未必是模型优化好的 切入点;反之,如果你的方案是和运营同学看 bad case,梳理现阶段的作案通用手段,并通 过分析上线一个简单模型或者业务规则的补丁,就明智很多。如果上线后,案件量真掉了下 来,就算你的方案准确率很糟,方法很 low,但你解决了业务问题,这才是最重要的。 虽然业务目标很关键,不过一般讲,业务运营同学真的不太懂得如何和技术有效的沟通业务 目标,比如: 我们想做一个线下门店风险评级的项目,希望你们通过反作弊模型角度帮我们把门店打个分 (问题:风险是怎么定义的?为什么要做风险评级,更大的业务目标是什么,怎么排期的? 这个风险和我们反作弊模型之间的业务关系,你是怎么看的?) 是否可以做一个区域未来 10min 的配送时间预估模型?我们想通过你们的模型衡量在恶劣 天气的时候每个区域的运力是否被击穿 (业务现状和排期?运力被击穿可以扫下盲么?运 力击穿和配送时间之间是个什么业务逻辑,时间预估是刻画运力紧张度的最有效手段么?项 目的关键场景是恶劣天气的话,我们仅仅训练恶劣天气场景的时间预估模型是否就好了?) 为了保证整个技术项目没有做偏,项目一开始,一定和业务聊清楚三件事情: 1、业务核心问题、关键场景是什么 2、如何评估该项目的成功?指标是什么 3、通过项目输出什么关键信息给到业务,业务如何运营这个信息从而达到业务目标? 携程技术沙龙云海机器学习专场 461 项目过程中,也要时刻回到业务,检查项目的健康度。 三、数据、特征 要说正确的业务理解和切入,在为技术项目保驾护航,数据、特征便是一个模型项目性能方 面的天花板。 garbage in, garbage out 就在说这个问题。这两天有位听众微信问我一个很难回答的问题, 大概意思是,数据是特征拼起来构成的集合嘛,所以这不是两个要素。从逻辑上面讲,数据 的确是一列一列的特征,不过数据与特征在概念层面是不同的:数据是已经采集的信息,特 征是以兼容模型、最优化为目标对数据进行加工。就比如通过 word2vec 将非结构化数据结 构化,就是将数据转化为特征的过程。 所以,我更认为特征工程是基于数据的一个非常精细、刻意的加工过程。从传统的特征转换、 交互,到 embedding、word2vec、高维分类变量数值化,最终目的都是更好的去利用现有 的数据。之前有聊到的将推荐算法引入有监督学习模型优化中的做法,就是在把两个本不可 用的高维 ID 类变量变成可用的数值变量。 不过我普遍观察到自己和童鞋在特征工程中遇到的问题,比如,特征设计不全面,没有耐心 把现有特征做得细致... 也整理出来一套方法论,仅做参考: 图 3 变量体系、研发流程 在特征设计的时候,有两个点可以帮助我们把特征想的更全面: 1、现有的基础数据 2、业务“二维图” 携程技术沙龙云海机器学习专场 462 这两个方面的整合,就是一个变量的体系。变量(特征),从技术层面是加工数据,而从业 务层面,实际在反应 RD 的业务理解和数据刻画业务能力。“二维图”,实际上未必是二维 的,更重要的是我们需要把业务整个流程抽象成几个核心的维度,举几个例子: 外卖配送时间业务 (维度甲:配送的环节,骑手到点、商家出餐、骑手配送、交付用户;维 度乙:颗粒度,订单粒度、商家粒度、区域城市粒度;维度丙:配送类型,众包、自营...) 反作弊变量体系(维度甲:作弊环节,登录、注册、实名、转账、交易、参与营销活动、改 密... 乙:作弊介质,账户、设备、IP、wifi、银行卡...) 通过这些维度,你就可以展开一个“二维图”,把现有你可以想到的特征填上去,你一定会 发现很多空白,比如下图,那么在哪里还是特征设计的盲点就一目了然: 图 4 账户维度在转账、红包方面的特征很少;没有考虑 wifi 这个媒介;客满与事件数据没 考虑。 数据、和特征决定了模型性能的天花板。deep learning 当下在图像、语音、机器翻译、自动 驾驶等领域非常火,但是 deeplearning 在生物信息、基因学这个领域就不是热词:这背后是 因为在前者,我们已经知道数据从哪里来,怎么采集,这些数据带来的信息基本满足了模型 做非常准确的识别;而后者,即便有了上亿个人体碱基构成的基因编码,技术选型还是不能 长驱直入--超高的数据采集成本,人后天的行为数据的获取壁垒等一系列的问题,注定当下 这个阶段在生物信息领域,人工智能能发出的声音很微弱,更大的舞台留给了生物学、临床 医学、统计学。 四、模型 携程技术沙龙云海机器学习专场 463 图 5 满房开房的技术选型、特征工程 roadmap 模型这件事儿,许多时候追求的不仅仅是准确率,通常还有业务这一层更大的约束。如果你 在做一些需要强业务可解释的模型,比如定价和反作弊,那实在没必要上一个黑箱模型来为 难业务。这时候,统计学习模型就很有用,这种情况下,比拼性能的话,我觉得下面这个不 等式通常成立:glmnet > LASSO >= Ridge > LR/Logistic. 相比最基本的 LR/Logistic,ridge 通过正则化约束缓解了 LR 在过拟合方面的问题,lasso 更是通过 L1 约束做类似变量选择的 工作。 不过两个算法的痛点是很难决定最优的约束强度,glmnet 是 Stanford 给出的一套非常高效 的解决方案。所以目前,我认为线性结构的模型,glmnet 的痛点是最少的,在 R、Python、 Spark 上面都开源了。 如果我们开发复杂模型,通常成立第二个不等式 RF <= GBDT <= xgboost. 拿数据说话,29 个 kaggle 公开的 winner solution 里面,17 个使用了类似 gbdt 这样的 boosting 框架,其次 是 DNN,RF 的做法在 kaggle 里面非常少见。 RF 和 GBDT 的雏形,CART 是两位作者在 84 年合作推出的。但是在 90 年代在发展模型集成 思想 the ensemble 的时候,两位作者代表着两个至今也很主流的派系:stacking/ bagging & boosting. 一种是把相互独立的 cart (randomized variables, bootstrapsamples)水平铺开,一种是深 耕的 boosting,在拟合完整体后更有在局部长尾精细刻画的能力。同时,gbdt 模型相比 rf 更加简单,内存占用小,这都是业界喜欢的性质。xgboost 在模型的轻量化和快速训练上又 做了进一步的工作,也是目前我们比较喜欢尝试的模型。 携程技术沙龙云海机器学习专场 464 图 6 The Child of RF&GBDT 携程技术沙龙云海机器学习专场 465 携程酒店浏览客户流失概率预测 [作者简介]陈无忌,就读于中国科学技术大学计算机学院,15 级硕士研究生。研究方向机 器学习、大数据、智能交通等。在校期间多次参加大数据竞赛,在携程云海平台比赛中, 两次和队伍一起获得第一名。 现场视频:https://v.qq.com/x/page/v0500kpd68w.html 客户流失率是考量是业务成绩的一个非常关键的指标。根据历史数据建立模型,使用机器 学习的方法预测客户流失概率,可以找出用户流失的因素,从而完善产品,减少客户流失 概率。 那么,对于这样的一个问题,我们需要做哪些数据分析?特征又是如何提取?如何选择合 适的机器学习模型?如何调整模型的参数?同时对于类似的这些问题,又有什么常见的套 路呢?本文将基于客户流失率预测的赛题,以及个人的实战经验,对上述的问题一一做出 解答。 接下来,将从以下几个方面对客户流失率预测这个问题进行阐述:首先,对现有的赛题和 数据进行了一个简要的分析;然后是特征工程的介绍,着重介绍了针对现有的数据如何有 效地提取特征;第三部分是模型及其原理的介绍,介绍了 GBDT 的原理,以及 XGBoost 的 使用以及调参方法;第四部分介绍了常见的模型融合的方法,包括 Bagging 和 Stacking, 以及在本赛题中融合的架构;最后一部分是经验的总结,结合个人多次的参加大数据竞赛 的实战经验,分享了相关经验。 一、问题分析 拿到一个机器学习问题,我们首先需要明白我们要解决一个什么问题。在云海竞赛平台的 官方网站上给的赛题描述是非常模糊的一段话(深入了解...找到...),看完这个可能还是不 明白需要干什么。没关系,继续看数据,数据如下图所示。数据上面有个 label,是 1 表示 客户最后流失了,是 0 的话表示最后客户没有流失。看到这里,于是明白了,这是一个分 类的预测问题。 携程技术沙龙云海机器学习专场 466 接着需要关注赛题的评价标准,对于任何比赛,评价标准一直是一个很重要的东西。评价 标准即损失函数,直接决定了我们后边分类器的学习目标。对于一个分类的问题而言,最 终的评价标准是准确率和召回率的一个组合,常见的就是 F1 值或者加权的 F1 值。对于这 个比赛,评价标准是要求在达到 97%的准确率的情况下,可以使得最后的召回率尽可能的 高。这个也很好理解,和携程的业务关系密不可分。 对于客户流失概率而言,我“宁可错杀三千,也不可放过一个”。就是说,我是要尽可能地 采取相关的措施,一定不能允许有客户流失的情况发生。同时,因为挽救可能流失的客户 需要成本,所以我也要求尽可能高的召回率。 接下来关注的是数据的具体情况。数据的特征除了 id 和 label 以外大致可以分为三类,一 携程技术沙龙云海机器学习专场 467 种是订单本身的特征,比如订单的预定日期以及订单的入住日期等;另外一种是和用户相 关的特征;还有一类特征是和酒店相关的特征,比如酒店的点评人数、酒店的星级偏好 等。 同时,我们会注意到官方对于赛题数据,曾经做过这样一个解答:如果一个用户浏览了 A、B、C、D 四个酒店,最后选择了第四个酒店的话,那么会产生四条记录,并且这四条 记录的 label 都会被标记为 1。 从这一点我们可以看出,我们在做本地测试集划分的时候需要基于用户进行划分,也就是 要保证划分前后的数据是满足独立同分布的。但是很遗憾,为了保护用户的隐私,并没有 提供 User ID 的数据。因此,我们采取了一种近似的方法,就是看所有和用户相关的属 性,如果这些属性都是相同的,那么我们就认为这是一个用户的行为。 我们基于上述原则,将原始训练数据集的三分之二划分为训练数据集,三分之一划分为本 地测试集。因为这个场景下的时间序列特性不是很明显,所以没有按时间线对数据进行划 分。在特征选择和调参的过程中我们主要使用线下的数据集进行的。 我们整个的工作流程如下图所示。线上的测评每天只有一次机会,我们在线下每天进行多 次的特征选择和参数调整。整个的流程包括特征的抽取、数据集重采样、特征选择和模型 的融合。 二、特征工程 整个特征工程的总体流程如下所示,相关的数据已经由比赛的主办方提取完成。我们在本 次比赛中主要进行了数据采样一直到模型评估中间的工作。在生成了所有特征之后,我们 会进行特征选择,我们通过本地的测试集对特征进行评估,保留有效的特征,去除无用的 特征。 携程技术沙龙云海机器学习专场 468 首先要进行缺失值的填充工作,从下图的数据中我们看到,有大量的缺失值分布在各个特 征中。一般情况下填充缺失值的方法是使用均值或者 0 进行填充。我们在这里用 0 填充。 但是有些属性,比如用户年订单量这样的特征,用 0 填充的话会产生混淆,填充之后无法 判断是本身是 0 还是后来填充的 0。所以我们对于这类特征,在填充 0 的基础上,构造一 列新的特征,标记原始的数据中的 0 是否是我们填充得到的。 然后是特征的二值化以及特征的多项式变换,二值化主要使用了 one-hot。前者将一些用 数字枚举的特征变换成多维的 0-1 编码,后者根据现有特征演化出相关的特征。这一类的 特征可以直接通过调用 Sklearn 的相关函数实现。 携程技术沙龙云海机器学习专场 469 在第一部分的数据分析中,提到了这个数据跟用户是息息相关的。所以在这里在对数据进 行用户分组之后,需要提出每组的最大值和最小值作为特征,如下图例子所示。 另外还有聚类特征,整个数据集中重要的两个部分,一个是用户相关的数据,一个是酒店 相关的数据。因此,我们把这两类主体进行一个聚类,把类的标签号作为一个新的特征。 对用户和酒店我们分别使用以下的特征进行聚类。 最后还有一类特征是根据现有特征衍生出来的一些特征,比如访问日期和实际入住日期之 间的差值,还有访问日期和入住日期是不是周末等特征。在机器学习中,是否为周末这个 携程技术沙龙云海机器学习专场 470 特征往往是非常重要的。 三、模型原理及调参 对于一个分类问题,一般经常使用如下的一些模型。在正常的情况下,GBDT 会较 Random Forest 有更好的效果,但是如果数据的噪声比较大的话,也会出现 Random Forest 的效果更好的情况。XGBoost 是一个机器学习的框架,主要集成了 GBDT 算法。在本次比 赛中,我们主要使用 XGBoost 作为我们的分类器。 首先先简单介绍一下 GBDT 分类器的原理,GBDT 的核心就在于,每一棵树学的是之前所 有树结论和的残差,这个残差就是一个加预测值后能得真实值的累加量。对于残差,一般 的计算公式为。但是为了使得这个残差更具有普遍性,所以实际中往往是基于 loss Function 在函数空间的的负梯度学习。所以严格说,GredientBoosting 算法是基于伪残差 的学习。实际上 GBDT 算法是 GB 算法的一个特例,只是说它的弱分类器是决策树分类器 而已。具体 Gredient Boosting 的算法如下图所示: 携程技术沙龙云海机器学习专场 471 具体的 GBDT 算法描述如下:首先给所有的样本一个初始值,0 或者任意。然后使用一阶 导的负梯度的函数计算伪残差。接着使用一个弱分类器(决策树)来对上面的残差训练, 得到一个弱分类器能够最好地对残差进行拟合,就是上面的 h(x)函数。 XGBoost 相比较 GBDT 做了很多的改进。虽然说基分类器还是 GBDT,但是 XGBoost 有几 个优化,一个是在目标函数中加入了正则项,防止过拟合。通过泰勒展开,最后目标函数 变成一个跟一阶导和二阶导相关的式子。关于正则项的部分主要是叶子节点的数量以及叶 子节点的 loss。除此之外还有其他的一些优化,包括对列进行了抽样,使用了随机森林的 思想。然后在对叶子节点进行分裂之前提前进行了排序,很大程度地提高了效率。 下图中红色箭头指向的 l 即为损失函数;红色方框为正则项,包括 L1、L2;红色圆圈为常 数项。XGBoost 利用泰勒展开三项,做一个近似,我们可以很清晰地看到,最终的目标函 数只依赖于每个数据点的在误差函数上的一阶导数和二阶导数。 携程技术沙龙云海机器学习专场 472 对于一个 XGBoost 模型而言,我们有这么多的参数,那么怎么来调整模型的参数,使得单 个的模型表现更好呢。根据以往比赛的一些经验,主要调整了树的最大深度,学习的速 率,还有树的个数这三个参数来进行调整,调整的方法主要是用 GridSearch,通俗的说就 是有点像是穷举的意思,遍历尝试找出最优的一些参数的组合。 对于调参而言,不用过于重视在验证集上面的表现,因为毕竟验证集的分布和测试集的分 布还是有一定的差别的,为了保证模型的泛化能力,所以我们在调参的时候调到差不多就 行。另外一个方面,在选取最优参数的组合做 ensemble learning 的时候,尽量选择参数差 别较大的一类组合做 ensemble,因为差别较大的一组做集成学习的话效果会比较好。 四、模型融合 接下来介绍的模型融合方法。下面的流程图展示了如何进行模型融合。 首先,通过有放回的随机抽样的方法,按照原来数据集正负样本的比例进行随机抽样,从 原始数据集中进行抽样,获得了五个训练集。然后对这五个训练数据集分别使用 XGBoost 分类器进行训练。XGBoost 的参数为在前面本地验证集上面采用 GridSearch 得到的最优的 参数。最后使用训练出来的五个模型分别对线上的测试集进行预测,最后将预测得到的结 果直接取平均,从而得到最终的结果。 当时,就这个最后的融合而言,有的是直接取平均,有的会按照一定的比例融合。具体的 比例依据,有时候是通过尝试不同的比例看线上的结果,同时也要基于模型的原理。对于 模型的融合方法的话,我们在这里一般考虑 Stacking 或者是 Bagging。我们在本次比赛中 采用了 Bagging 的方法。所谓的 Stacking 就是把各个模型的预测结果,再做一个逻辑回归 的模型,而不是简单的平均。 携程技术沙龙云海机器学习专场 473 现在来讨论为什么说模型融合在这里能够提升模型的效果呢。对于模型的误差来说,一般 是两部分的误差,一种是偏差,一种是方差。只有两者都小了才能保证模型拥有最小的整 体误差。所谓的偏差是由于模型对于数据集的学习还不到位导致的欠拟合,所谓的方差指 的是由于模型对于训练数据集的学习太好了,所以这就导致了在训练数据集上面表现很 好,但是到了实际的测试数据集上面表现欠佳,就是传说中的过拟合。 那么这个跟我们的模型提升又有什么关系呢。因为在前面提到了,XGBoost 采用的 GBDT 算法是对残差进行的拟合,所以说 XGBoost 的模型是拥有比较小的偏差,但是方差可能就 会相对高一点,就是我们感觉会有一些过拟合在里面。所以采取再进行一次 Bagging 的原 因。其实从上图的原理可以看出,前面一幅图的原理是很像随机森林的原理的,只是前面 的单分类器用的是 XGBoost。 携程技术沙龙云海机器学习专场 474 五、总结 在前文中,从特征选择,模型原理,模型融合等方面进行了一个简单的概括。那么,对于 一个机器学习的问题,或者说对于一个大数据竞赛,我们有什么样的相关经验可以总结 呢。 首先一点就是要注意数据分析,数据分析是很重要的。数据分析的相关工作包括,我拿到 这样一个数据,我需要做一些基本的统计,比如最基本的,在这个问题中,正负样本的比 例多少要统计;还有属性的缺失值是不是严重,属性的缺失值的部分占数据总量的比例是 多少,以便及时决定是否使用这个属性,或者说采取加特征或者是其他的什么方式。 还比如其他的问题,如果是一个回归的问题,我也需要分析数据,比如我根据歌手的历史 被收听的数量来预测这个歌手未来一段时间被收听的数量这样一个问题,我要做好前期的 数据分析,包括画出趋势变化图,看看曲线的波动是怎么样的,如果波动大,要看看波动 有没有周期性,如果有周期性,要分析是什么样的周期性。 这样直接决定了后面是采用什么样的模型,怎么样提特征,是否加入什么样的规则。还有 就是数据的预处理,比如刚才说的缺失值的填充的问题,还有如果正负样本严重不均的 话,还要再进行一些重采样。还有一个更好的例子,在讯飞做的道路匹配的项目中,仅仅 通过对地图的预处理最后将匹配准确度提高了百分之二十。 然后关于特征,特征决定了模型的上限是多少。在实际的机器学习过程中,模型以及调参 最后能够提升的余地十分有限,关键还要选好特征。要想选好一个好的特征,需要对业务 相关的问题十分了解,这一点需要和相关的产品以及这个问题的领域专家做好沟通。 关于调参,想要强调的一点是一定要在特征工程做好了之后在进行模型的调参,根据不同 的特征模型的最优参数是不一样的。然后在模型调参这里不用花费太多功夫,因为模型调 携程技术沙龙云海机器学习专场 475 参的提升余地实在是十分有限。 不过在调模型的时候,有一个地方是可以提升的,就是改模型的 Loss,将模型的训练目标 函数变成最终业务要求的评价函数,能够使得模型在当前评价标准下获得最优的表现。当 然,不是每个评价函数都存在一阶导和二阶导,实际处理的过程中可能会推导出一个近似 的方法。 最后是关于融合。虽然说模型融合十分有效,但是不建议一开始就进行模型的融合,可以 把模型融合作为最后的杀手锏。在前期还是要尽可能的把单模型调好。一般单模型效果好 的话,最后融合的结果都还比较理想。 携程技术沙龙云海机器学习专场 476 去哪儿酒店算法服务平台 [作者简介]张中原,2011 年加入去哪儿网,先后从事交易系统、酒店数据、公司基础平台 与组件、存储和监控等相关工作。 现场视频:https://v.qq.com/x/page/w0500b6tfbs.html 最近几年时间,机器学习算法相关的话题异常火爆,各个具有一定规模的公司都开始成立 算法组开展相应的工作,从"对大数据的分析观察"逐渐转变为"用数据和算法驱动业务的发 展"。 对于机器学习技术本身互联网上有足够的学习资料和案例,甚至已经有将机器学习做成服 务的 AI 云出现,这其实也反映出行业对实施机器学习相关配套系统的迫切需求。 我们团队在做这件事情的过程中,也遇到过一些问题和解决思路,在此跟大家做个分享。 一、算法平台 整体上我们的算法平台建设分为三个阶段: 1、优先解决算法部署的问题,降低跨团队沟通障碍; 2、有了一定的积累之后,再完善一套较为完整的特征集,避免每个模型重复提取数据以及 实时特征收集; 3、最后打造模型训练系统,以快速的针对多种算法结果进行评测,选取最优结果。 目前第一阶段基本工作基本结束,第二阶段正在进行中,本文主要介绍第一阶段有关的内 容。 二、部署的问题 通常实施一个算法需要进行数据收集、特征处理、模型训练和模型发布这几个步骤,其中 前三个步骤初期都可以离线执行,团队内部并行即可,而最后一步需要和其他团队合作发 布,这其中有较多排期、协调、沟通等障碍,算法的发布模型迭代周期较长,主要面临的 问题有: 1、没有标准规范,管理较为混乱,模型工具五花八门; 2、模型训练时使用 Python 进行特征转换,而发布是交易系统都是 Java 体系,转换逻辑需 要重新开发,且极易引起逻辑不一致的情况; 3、每个模型的生成和发布过程大致相同,之间有很多重复性的工作无法复用; 携程技术沙龙云海机器学习专场 477 4、算法通常不可能一次成型,都有一个逐步迭代的过程,这就要引入版本的概念,考虑每 次变化对业务系统的影响,是否需要排期调整等; 5、由于初期模型都是直接交给业务系统加载使用,出现异常案例时,跟踪调试较为复杂, 且算法团队无法对模型运行状况进行监控和在线调试,问题排查困难。 所以我们急切的需要一个系统能够将部分重复性工作自动化执行,统一调用接口减少与业 务组的沟通成本,同时进行模型的跟踪与调试便于问题排查。 三、算法描述 平台构建初期,需要对算法有一个明确的定义,一个算法主要有特征处理和模型运算两部 分构成。 首先,算法需要有版本,这很容易理解;其次,跟具体的业务场景有关,比如我们面向的 是旅游行业相关的,有地域性差异,每套数据训练出的模型文件不同,所以需要考虑模型 的细分粒度,我们使用一个 filter 作为城市标注;然后需要兼容多种类型的模型;最后还要 统一特征转换,使一套转换逻辑可以应用到所有场景。这里有几个主要的接口定义: interface Algorithm { StringgetApp(); StringgetVersion(); FeatureResolver getFeatureResolver(); EvaluatorgetEvaluator(); ListenableFuture> eval(Request request); } interface AlgorithmFactory { AlgorithmgetOrCreate(String app, String version, String filter); } 四、模型定义 在考虑使用何种模型文件时,我们做过一些对比。 早期的几个模型选用 PMML 文件的方式,其本身包含完整的特征预处理、模型预测和后处 理的描述,但后来发现我们其实只需要模型预测的功能,而更希望将特征转换独立出来使 用; 之后的几个模型选取的是 Vowpal Wabbit,主要原因是经过工业应用验证,且有着更好的 性能;然而 VowpalWabbit 官方主要使用 C++实现,针对 Java 只提供了 JNI 的包装,所以 我们根据源码自己实现了模型预测逻辑的 Java 版本,将 vw 的模型文件直接加载来用。 携程技术沙龙云海机器学习专场 478 此外还有一种历史遗留下的 XML 文件描述的模型 DataProc,其思想与 PMML 类似,也需 要兼容。除了可描述的模型,还有可能出现多个模型协同运算的场景,目前我们采用编写 少量代码方案,将多个模型进行简单组装,对外接口与单个模型保持一致。 interface Evaluator { ResultValue eval(F resolved); } 此外这里还提供了一系列接口用于外部资源注入,如: 访问特征集的 KVStoreSupport,创建 子算法的 AlgorithmFactoryAware 等 public class EvaluatorFactory { publicEvaluator createEvaluator(Config config, String filter, InputStream model) { Evaluator e = createInstance(...); if (einstanceof AlgorithmFactoryAware) { ((AlgorithmFactoryAware) e).setAlgorithmFactory(...); } if (einstanceof KVStoreSupport) { ((KVStoreSupport) e).setStore(...); } ... } } 由于 vw 模型文件一般较小,可以直接加载使用,而有的模型文件很大,直接使用对 gc 会 有严重影响,导致若干请求超时甚至引起服务恶化,需要考虑使用堆外内存的方式。 五、特征处理 前面提到,我们需要将特征转换的功能独立出来,达到一套逻辑多处使用,一处调整全部 生效的目的;我们从 Airbnb 提供的 Aerosolve 项目中得到启发,将所有的特征结构表示为 一个 FeatureVector,将所有转换逻辑封装成一个个 Transform 的实现。 根据使用场景,实现对应的 Parser, Writer 将外部数据和 FeatureVector 结构之间进行转 换,使用 aerosolve 语法描述整个特征处理的过程。在实现 Transform 类时,遵循"一个类 只做好一件事"的原则。为了提高 Transform 的使用灵活性,我们引入了表达式的概念,根 据配置中'$'的符号标识,根据上下文场景调整转换逻辑,如: # 气温转换 category_temperature { 携程技术沙龙云海机器学习专场 479 transform: category # type:raw # fields:[""] keys:[temperature] # 使用默认命名空间中的 temperature 特征 #outputType: float # 默认将 Raw 类型转换到 Float output:all # 输出到 all 空间 outputKey: $key # 使用原有特征名,支持 $value, $category 或固定值 outputValue: $category # 使用类别作为特征值 # keep:false # 是否保留原有特征数据 categories: { '' : 0 '<10' : 1 '<30' : 2 '>=30': 3 } } 使用上述配置的一个转换的例子效果如下: raw: {"" :{"temperature": "26"}} # 转换前 floats: {"all": {"temperature":2}} # 转换后 使用 list 转换器,将一系列的转换配置组装起来,就能做到任意特征转换的描述。通过若 干模型的发布积累,目前已有 30 多个转换器,总结下来有几个比较常用:  default 缺省值设置,这个容易理解  category 用于归一化处理  store 访问外部存储进行特征展开,如: 使用 uid 获取用户完整数据  cross 将 FeatureVector 内两个特征向量进行交叉运算 为了获取更好的性能和存储空间优化,我们使用了 koloboke, trove4 里面的类代替 Java 自 带的集合对象,以支持原生的 int, double 等类型。由于每个 Transform 类都是经过长期打 磨改进,积累下来,有效的保证了特征转换的性能和质量。针对 Hive 引用特征转换逻辑, 我们实现了对应的 UDF,开发阶段时使用本地转换配置,上线后使用 hdfs 或算法平台中的 配置。 六、服务的使用 平台演进初期,我们只负责维护模型文件和特征,由业务系统负责加载模型运算,经历了 几个项目后遇到几个棘手的问题。 携程技术沙龙云海机器学习专场 480 首先,模型文件一旦交付,算法组没办法了解到其真实的运行状况,无法做到很好的监 控;一旦出现问题,也无法进行排查和调试,系统边界模糊,沟通成本高。 所以我们将算法平台直接做了服务化,这样一来业务开发在接入算法时,和使用其他服务 没有任何区别,知道传什么参数就行了,而模型的变更、调试、监控则全部由算法组负 责,对问题排查和后续的优化提升都更方便,减少沟通成本。 AlgorithmBuilder builder =AlgorithmBuilder.create().setServer(...). setStore(...).setLazyInit(true/fal se).setExecutor(...); Algorithm algorithm = builder.getOrCreate(app,version, filter); interface AlgorithmService { ResultValue eval(Request request); } 为了方便调试我们在请求中增加了 debug 开关,通过线程绑定 RequestContext 的方式, 可以收集各阶段的数据状态和运算结果一并返回。同时还收集汇总了各个算法服务器上的 日志信息,可以在平台管理页面追查某个请求引发的个阶段的运行信息。 七、总结 算法平台的构建也是一个逐步演进的过程,从特征工程到服务提供是一项浩大复杂的工 程,每个人都要根据各自的技术选型和场景进行不断的优化,但"以服务业务为核心不断提 高生产力"的宗旨不会改变。本文只是冰山一角,阐述机器学习在实际应用中最后一步时遇 到的问题和解决思路,希望能对有同样问题的同学提供参考。 携程技术沙龙云海机器学习专场 481 机器学习在 1 号店商品匹配中的实践 [作者简介]刘洋,1 号店搜索部算法工程师,机器学习的爱好者和实践者。上海大学博士, 在语义分析、知识发现有深入研究。本文来自刘洋在“携程技术沙龙——云海机器学习 Meetup”上的分享。 现场视频:https://v.qq.com/x/page/j05005sw9ko.html 电子商务通过服务和商品给用户带来极致体验。其中,服务包括用户的浏览体验、配送体 验和客服体验。而商品则包括了商品质量、商品价格以及商品丰富度等。 所谓知己知彼,及时了解友商的商品信息对于电商的发展至关重要。这其中,从海量的商 品信息中发现商品间的匹配关系,特别是不同网站间的商品匹配关系,在商品定价、商品 选品、类目挂靠等场景中发挥着基础性作用。 一、电商领域商品匹配问题的特点 1、不同于发现相似商品,发现完全匹配的商品要求商品中所有信息是一致的,没有冲突, 可见商品匹配难度高。 2、我们通过商品标题进行商品匹配,商品标题文本短,每个词都很重要。有的时候多一个 词、少一个词都可能导致是不匹配的。例如这两组商品,同样是因为“有机”这个词,上 面一组商品是匹配的,下面的黑豆和有机黑豆缺是不匹配的。 目前各家网站运营的规范不一样,有的时候人也无法直接从标题中分辨出两个商品是否是 携程技术沙龙云海机器学习专场 482 匹配的。另外,在我们的场景中,一旦两个商品构成匹配关系,除非商品下架,两个商品 的匹配关系不大可能会发生改变。 二、基于人工规则的商品匹配 通过规则的商品匹配,比较两个商品的主要信息,例如比较判断品牌、口味、重量等信息 是否一致,如果都是一致的,则两个商品是匹配的。规则匹配每次都要去分析,两个商品 信息中,匹配的有哪些,不匹配的有哪些,用什么规则可以进行区别。 优点:易于干预,匹配错误的 Case 易于调整。 缺点:当规则的树分叉到达一定量级时人工维护规则模型会变得很困难,并且树分叉的优 先级难以判定。 三、基于特征工程的商品匹配 携程技术沙龙云海机器学习专场 483 特征工程是把原始数据转变成特征的过程。这些特征能很好的描述这些数据,利用它们建 立的模型在未知数据上的表现性能可以接近最佳性能。基于特征工程的商品匹配,是从两 个商品标题中人工定义一些特征,比如从两个商品标题上的品牌是否一致、颜色是否一 致、口味是否一致等维度进行打分,利用这些打分特征,通过监督模型进行训练和预测。 优点:关注在特征和模型,选择的特征越好,模型越简单,最终性能也就越好。 缺点:发现好特征比较困难,如果特征构建做的不好,会直接影响模型性能。 四、纯数据驱动的商品匹配 携程技术沙龙云海机器学习专场 484 纯数据驱动的商品匹配,不去人工定义特征,而是将每个词都作为一个特征让模型去学 习。如图所示,我们将商品标题中每个词都作为一个特征,特征的数量多,经独热编码后 每个标题数据稀疏。 我们选取 Factorization Machine(FM)模型,作为纯数据驱动方法使用的模型。因为商品 匹配中两两词的特征组合有助于判断商品是否匹配,FM 模型适合解决稀疏矩阵特征组合 问题。FM 模型将特征通过隐向量进行表示,其组合项的参数是两个特征的隐向量的点 积,二次项参数个数远少于二阶多项式模型的参数数量,易于训练。另外,FM 模型高 效,可在线性时间训练和预测。 训练样本 一组商品是否匹配构成一个样本。如果这两个商品是匹配的,标签是 1,否则是 0。两个 商品标题切分词后的每个词都作为样本特征。同一个词有可能来自 1 号店商品标题,也有 可能来自友商商品标题,这里作为两个不同的特征。 例如特征“480773:YHD_BRAND:康师傅”,480773 是特征的编号,“YHD”表示来自一号 店商品的特征,“BRAND”是特征的词性表明是品牌词,最后“康师傅”是具体的词。 训练技巧 避免失衡的正负样本比例。我们的场景正负样本比例在 1:70 左右,训练中每轮都对负样本 进行采样,使得每轮训练使用的正负样本比例在 1:2、1:3; 随机梯度下降需要打乱样本顺序。对每轮训练使用的正负样本顺序都进行了 shuffle。 携程技术沙龙云海机器学习专场 485 确保训练充分。每轮训练后模型在训练集、测试集上的评价指标进行输出。 纯数据驱动的商品匹配优化(1)——线性项部分去除 利用原始 FM 模型去做纯数据驱动匹配的结果很差,基本处于不可用状态。需要进行优 化。优化(1)将 FM 模型线性项部分去掉。因为线性项的意义是单个特征对商品匹配的贡 献。而单纯从一个个特征来看,无法判断两商品是否匹配。于是,我们将线性项部分去 掉。 纯数据驱动的商品匹配优化(2)——交叉项限定在两个商品的特征间进行组合 携程技术沙龙云海机器学习专场 486 在优化(1)的基础上,优化(2)限定 FM 模型的组合项(交叉项)部分,组合项的两个 特征需要分别来自两个商品,一个商品下的两两特征不进行组合。因为单纯从某个商品下 的特征组合,无法判断两商品是否匹配。经过优化,模型性能得到一定提升。 纯数据驱动的商品匹配优化(3)——交叉项限定在同一词性的特征间进行组合 在上述优化基础上,优化(3)继续限定 FM 模型的组合项部分,组合项的两个特征需要是 同一词性,两特征词性不一样不进行组合。因为不同词性的特征组合无法判断两商品是否 匹配。经过优化,模型性能进一步得到提升。 针对商品匹配问题,先是去掉了 FM 模型的线性项,然后对模型的组合项进行了限制。本 质上,是通过对问题的思考,人为地加了先验知识,更有针对性地优化模型。 纯数据驱动的商品匹配优化(4)——相同词使用同一隐向量表示 携程技术沙龙云海机器学习专场 487 基于上述模型的优化,优化(4)是对特征隐向量的优化。我们希望相同词的特征组合的交 叉项系数要大,这就意味着它们的点积要大,两特征的隐向量距离要近。优化(4)将两商 品中相同词使用同一隐向量表示,此时两个特征向量距离最近,构成的组合项打分要高。 经过改进,模型的性能进一步得到提升。 纯数据驱动的商品匹配的优缺点: 优点:不需要人工去定义特征。 缺点:干预难,对于错误样本难以进行有效干预。 五、展望 1 号店采用了规则匹配、特征工程匹配、纯数据驱动匹配去解决商品匹配问题。目前多是 基于文本信息,由于各家网站的运营规范不同,有时仅凭借标题等信息也无法判断两个商 品是否匹配。后续期望加入商品图片等商品信息,通过异构信息,进一步提升商品匹配的 准确率和召回率,帮助运营人员减少人工成本。 携程技术沙龙敏捷专场 488 携程技术沙龙敏捷专场 携程技术沙龙敏捷专场 489 从四个案例看敏捷开发的持续改进 [作者简介]黎娟,去哪儿过程改进总监。15 年软件项目管理及过程改进经验,曾先后就职于 雅虎中国/阿里巴巴、腾讯、去哪儿网,擅长问题分析以及基于问题驱动的过程改进。 现场视频:https://v.qq.com/x/page/k0509s06sgl.html “敏捷”这个词近几年非常火,经常会有人问:“我们应该怎样开始做敏捷?”或者:“能 不能来帮我们推一下敏捷?”这种问题我通常都不敢轻易回答——敏捷有很多实践,管理 的、工程的都有,但敏捷绝非我们看到的站会、持续集成、TDD 等那么简单,真正的敏捷体 系是从理念到文化的一次变革。 所以具体到一个团队,究竟为什么要做敏捷,能够多大程度地承受改变所带来的痛苦和风险, 本质上还是需要自己先想清楚,我们要解决的问题或者期望的价值究竟是什么,再来判断应 该做什么以及怎么做。 这里分享几个敏捷相关的过程改进案例,希望能够给到大家一些可以借鉴的东西。 案例一:灵活地响应变化 两年前我在酒店事业部做支持的时候,有一次同一位产品总监聊天,他向我抱怨,说:“技 术团队怎么有那么多的项目延期?他们能不能靠谱一点,至少给我的周计划里,承诺要完成 的事情应该做到吧!” 我反问了一个问题:“那作为一个产品总监,你能不能保持你的团队一周内的需求不改变呢?” 他想了想说:“我做不到。” 我们相视一笑,很容易就达成了共识——我们不能指望不变或者少变,而是要提升整个团队 的应变能力,去响应甚至拥抱变化。 那么怎样才能提升团队响应变化的能力呢? 首先,要建立良好的可视性——能够清楚地知道每个人每天都在做什么,进展如何,何时能 完成——这样在发生变化时,我们才知道应该安排谁、什么时候去做是最合适的。 所以我们做 story/task 的拆分,并搭建看板,展示每个人的工作;再通过站会沟通进展以及 问题,保证信息每天都能得到更新。 携程技术沙龙敏捷专场 490 图 1:一个典型技术团队的看板 其次,要提升变更决策的效率——我们要让有价值的变更能够快速地得到实施,首先要有个 快速且有效的决策机制。那么应该由谁来做这个决策呢?决策人级别太高,决策效率会低; 决策人级别太低,又担心水平不够,不能服众。我的观点是“让具备这个能力的、级别最低 的人来做”——资深人员能做就不要让 leader 做,leader 能做就不要让总监做,总监能做就 不要让 VP 做。有做的不对或者不好的时候可以升级问题,但不要一开始就把职责都定在管 理者身上。 所以我们引入了 PO(ProductOwner)的概念。为每个团队指定一个 PO,他/她负责接收所 有的需求、判断价值和优先级;如果有需要的话,还要帮助其他产品经理做系统的需求分析。 大部分团队的 PO 都是资深的产品经理;但也有小部分技术团队,由于对应多个需求方,最 后技术 leader 自己做了 PO。在实践中我们发现资深的产品经理和技术 leader 都能把 PO 这 个角色做的很好,需要升级到管理决策的情况非常少。 最后,要降低变更管理的成本——变更过程中最令人感到痛苦的事情,就是 re-plan。一般 来说,如果只是调整当前项目的计划,还相对比较容易。但由于项目延期或者对人员的时间 占用增加,通常会导致其它项目的等待或延期,这种情况沟通起来就比较费时费劲了。更糟 糕的是,技术团队已经对计划做出了承诺,“客户”就会很愤怒:“你们这帮人怎么这么不 靠谱,承诺的事情为什么老是变!”所以很多技术 leader 会倾向于拒绝变更,他们会说: “你们先去走个流程!”“你们先去找 VP 审批一下!” 所以我们转变做计划的思路,从承诺 everything 转向只承诺当前优先级最高的事情;从“事 推动人”转向“人拉动事”——也就是说我们尽量让每个人都工作在优先级最高的事情上; 做完一件事情,再去需求队列里取当前优先级最高的;再完成之后,再去取下一件……如此 循环。这样我们就把对变更的决策与制定工作计划这两件事情“解耦”了,让整个团队都能 够聚焦在变更所带来的价值,而不是痛苦和折腾的过程。 关于如何制定工作计划(就是我们通常说的“排期”),这里推荐一个实践——每天上午站 会结束之后,会有一个小型的计划会议(planning meeting)。如果前一天有人交付了手头的 工作,就留下来参加计划会议,leader 会综合待排期的需求优先级、当前人员的能力等因素 携程技术沙龙敏捷专场 491 安排他们的工作。原则上已经开始开发的工作尽量不要中断,除非有特别紧急的事情,leader 会暂停部分相对低优先级的工作,抽调人手响应紧急情况。一个 10 人以内的团队,平均每 天花费的时间不会超过 15 分钟。 这个案例是最轻量的敏捷实践,在不改变组织结构和工程流程的前提下,能够以最低的成本 实现灵活的应变能力。这也是目前在金融事业部和大住宿事业部绝大多数团队所采用的实 践。 案例二:高效地沟通与协同 有一天,专车事业部的一位总监找到我,说:“我们也很想做敏捷,你们能不能来帮个忙?” 于是,我安排了一位同事去支持这个团队。 她去到这个团队,开始尝试用我们一贯的方法,帮助团队搭建看板、开站会,却发现几乎所 有的 leader 一致认为站会是没有意义的——他们曾经尝试过,最后发现每日沟通花费了时 间,但却并不会给他们的工作带来多大的帮助,于是都放弃了。 为什么在那么多团队行之有效的实践,到了某些地方就失效了呢? 经过调研,我们找到了问题的答案——要找对需要在一起沟通的人,这比沟通的形式更重要。 专车的这个团队是个典型的 app 开发团队,分为 ios、android 和服务端开发三个组;维护 着两个主要的产品,分别是给用户下单用的“用户端”和供司机抢单用的“司机端”。 当我们按照组织结构召集一个团队开会的时候,团队成员之间由于工作的相关度不高,所以 并不太关心其他人干了什么,虽然站会时间不长,可大家觉得没有价值,是在浪费时间。 而当我们将团队重新组合之后,按照产品线划分成司机端、用户端和纯服务端三个团队,每 个团队都包括 PM、各种 DEV 以及 QA。调整完之后,大家沟通的积极性马上就高了起来, 因为大家是共同在做一件事情,是上下游的关系,相互能够给对方提供信息并解决问题。 最终我们其实只做了一件事情,就是建立了一个完整的交付团队(也就是通常人们说的“敏 捷团队”),所有的实践就顺理成章地实施起来了。从这里我们可以看到,敏捷的团队,是比 敏捷的流程或者实践更重要的东西。 那么,什么样的团队,才能算是一个敏捷的团队呢?Mike Cohn 给出过 9 个问题,用来判断 一个团队是否“结构优良”,有兴趣的同学可以去看相关的资料。这里给出几点我认为最重 要的特点: 第一,要有交付能力——能够独立交付完整的业务功能,这是一个敏捷团队的最基本的特性。 这就意味着团队必须包含交付所需要的全部角色(一般至少要有前后端开发、测试),或者 具备全部角色的能力。 第二,要“高内聚,低耦合”——团队所承接的需求,大部分都应该能在团队内部完成,对 携程技术沙龙敏捷专场 492 其他团队的依赖应该是少量的或者简单的;同时,应该尽量避免两个以上的团队共享一个成 员的情况。这样才能进一步地减少等待以及各种管理的浪费,保证快速交付的能力。 第三,要保持小团队——团队太大会降低沟通效率,增加管理的浪费;而小团队则更容易保 持对目标的专注并形成凝聚力。一般认为 7 加减 2 是比较合适的范围。 第四,要足够长期——团队需要一定的时间去形成统一的规则和文化。一个新团队度过磨合 期通常至少需要 2-3 个月的时间,而要想在这个基础上形成自组织、自管理的团队文化,则 需要更长的时间。只有成熟度非常高的组织(团队文化和基础设施都高度成熟),才能频繁 地变更团队的组成而不破坏团队文化。 这个案例是中等程度的敏捷实践,从过程改进的角度来看,仍然是属于渐进式优化的做法: 在保持组织结构不变的基础上,按照业务和产品的结构划分出“虚拟的”交付团队。在这个 团队里,各种角色能够频繁地、一致地、充分地交流,及时发现和解决的问题,从而快速交 付产品功能,实现业务价值。对这个实践目前执行到位的团队并不多,主要的原因是很多 FE 和 QA 团队作为“公共资源”没有与 DEV 进行对应的分拆,无法形成完整的交付团队。 案例三:目标一致的团队 去年下半年,我们在一个部门做支持的同学跟我反馈,说她觉得团队的流程在退化:原本分 拆了的 FE 和 QA 团队又合回去了,团队从原本的每日排期变回了周排期。跨团队的项目越 来越多,沟通、协调的过程也越来越复杂。整体的可视性在下降,管理的浪费在增加,当前 虽然还没有明显的问题,但长期发展下去团队的整体交付能力会下降。 当时我们的 CTO 正好坐在我旁边,问了一个问题:“如果一个流程是好的,它就不应该会 退化。如果它退化了,是不是说明有些什么地方是不够好的?退化的原因究竟是什么?” 为什么有些团队的流程,在过程支持的同学退出之后,就会慢慢退化呢?究竟是因为人的惰 性,还是流程的设计本身就有不合理或者不到位的地方呢? 经过对已经实施的团队流程的反复检视,我认为流程会退化的主要原因有两个: 一是这种虚拟团队的组建机制不够强壮——这种虚拟团队之所以能够形成,本质上是基于所 有的资源经理的承诺,这是一个不太稳固的基础。尤其是当资源团队的 leader 发生变动的 时候,新的 leader 没有经历过改进的过程,不理解为什么要这样做,或者不知道这种情况下 该怎么做,通常都会选择自己最熟悉的管理方式。这个时候,如果团队中没有一个真正懂得 这套体系的价值,并且能够强势坚持的人,流程就会发生退化。 二是团队的职责范围定的太窄——我们的交付团队最初建设的时候,基本上是基于一个系统 的,比如酒店交易的“用户系统”、“订单系统”。这在产品/系统建设的初期是没有问题的, 因为每个系统本身都还不完善,需要做的事情很多,大部分需求都可以由一个团队独立完成。 但是随着产品/系统的建设成熟,单个系统上可以做的东西越来越少,更多的需求会需要跨 多个系统修改,这时候团队的“高内聚,低耦合”特性就不存在了。 携程技术沙龙敏捷专场 493 那么怎样解决这样的问题呢? 今年 3 月中旬,我去上海与携程的技术与管理同学们做交流,有幸与大家公认的“敏捷做的 最好的团队”——酒店无线的技术团队做了一次关于敏捷实施的交流。在这里把他们的经验 分享给大家。 酒店无线的技术团队有 70 多人,组织架构按照技能划分为 iOS 团队、Android 团队、Service 团队和测试团队(见下图),存在的主要问题包括: 开发周期过长,从拿到 PRD 到交付要跨越一个月或者两个月。 需求变更频繁,2 个月内会有很多变化,导致已经开发的代码被浪费。 团队成员技能单一,不能根据需要互相援助。 开发团队只是被当作执行者,没有目标感和参与感。 最终这个团队选择了 scrum 的模型,将原有的组织结构整合重编,调整成了如图所示的一系 列的 scrumteam。每个 scrum team 都是完整的交付团队,包含 iOS、Android、Service 开发 和测试。并且鼓励一专多能,iOS 开发也会参与 Service 开发工作。每个团队分别对应不同 的目标(OKR),比如:用户/订单增长、基础体验优化、系统架构优化等等。 携程技术沙龙敏捷专场 494 经过这样的调整,大大缩减了沟通和管理的成本,提升了工作效率;团队聚焦于目标的实现, 会更加积极地参与目标的制定,团队的士气也得到了提高。 这种做法不仅解决了案例二中组织结构不稳固和职责范围过窄的问题,更好地方在于,它把 每个团队的目标和特定的业务目标明确地关联在一起,使得每个团队成员都有可能最大程度 地发挥主动性,帮助业务更好地取得成功——我们每年举办诸如 hackathon 之类的活动,希 望工程师们能够发挥创意,解决更多的业务问题。而事实上,如果能够提供更加有效的组织 结构和管理流程,工程师们每天都可以 hackathon。 这个案例是局部的比较彻底的敏捷实践,采用了组织变革的方式,将组织结构调整为基于交 付团队(敏捷团队)而非职能团队。并且在这个基础上,逐步形成“无边界”的工程师文化。 这个实践目前携程的酒店无线技术团队正在尝试,我们期待这个团队能够成为一个标杆,带 动大家走向更加开放、更加敏捷的文化。 案例四:强健的工程体系和文化 采用 scrum 的这种模式,显然在速度和灵活性方面有很大的优势。但是,天下没有白吃的午 餐,想要获得好处,就必然要付出代价。那么,敏捷要付出的代价是什么呢? 显而易见的,首先是资源问题。敏捷团队建设过程中最常见的情形之一,就是资源经理说“我 的人手不够,需要在不同的开发组之间协调资源,不然不够用”,“如果要拆下去的话,我们 需要多招 N 个人,还是现在这样效率高”。 所以这里就涉及到人的能力问题,一方面我们需要有招聘人的能力,以达到各种资源的合理 配比;另一方面,我们要有培养人的能力,鼓励团队成员成为“多面手”,每个人都能承担 至少 2 种角色,这样在有需要的时候可以在团队内部相互弥补。事实上,资源问题是个比较 容易解决的问题,只要大家有意愿、去努力,几乎所有的团队都能做到。 而真正严重的,其实是质量问题。很多强硬地推行了敏捷最后又退化的团队,绝大部分都是 因为 hold 不住质量。网上有很多这样的案例,大家可以自己搜着看。 携程技术沙龙敏捷专场 495 去哪儿酒店以前的交易系统 HMS,当时就是有 4 个独立的开发团队在上面工作,分别对应 不同的业务,就是因为没有控制住质量,最后不得不用上 100 多个人、几个月的时间,把系 统重新做了一遍,就是现在的 QTA。以至于团队的老成员们都有点心理阴影,一听说要让几 个团队修改同一个系统就紧张。 而我们之所以一直只在小范围做试点,没敢大规模地“推敏捷”,最担心的也是这个问题— —如果没有足够的把握,能在这种组织模式下保证系统的质量,即使我们“推”成功了,总 有一天还会要退回去的。 那么怎样保证质量呢? 这个时候就可以翻开敏捷的书籍们了,里面有各种各样的实践:单元测试、持续集成、 codereview……遵循这些实践,我们最终的目标是打造一个质量风险高度可控的工程体系, 并且在这个过程中提升人员的能力,形成团队的文化。 这里分享一下 Google 在工程体系方面的实践。 Google 的所有代码全部放在一个代码库中,除了搜索引擎的核心算法等少数模块,绝大部 分代码是没有权限控制的,所有人都可以修改。代码全部是主干开发以及源码依赖,自动化 的测试(包括单元测试)、持续集成和 code review 是必须的——每次提交代码时,系统会基 于最新的源码进行编译,并执行自动化测试。自动化测试不仅会“向下”执行,而且会根据 依赖关系“向上”执行所有可能被影响到的系统/模块的 test case。持续集成通过之后,还 要经过 code review,没有问题了代码才能真正入库。 这种做法保证了代码库的基础质量,建立了可以让“任何人随时修改任何代码”的体系能力。 这样的工程体系很好很强大,显然要投入相当多的资源和时间去建设,而且需要每一个工程 师时时刻刻都用心去维护,这就必须要形成工程质量的文化。 那么,什么是工程质量的文化呢?当我们提到敏捷的团队是面向目标而非系统时,很多 leader 的第一反应是:“系统没有 owner,质量怎么办?” 其实潜在的意思就是“是 owner 的人才会好好干,不是 owner 的人就会随便造。”这样的文化是不可能把敏捷做好的—— 系统没有 owner,就需要每个人都有 owner 的意识,这才是敏捷的文化!只有建立了这样的 文化,敏捷的体系才能够长期有效地运作。 最后分享一个关于文化的案例。 我在前一家公司(腾讯)工作的时候,团队也在实施全主干开发、全源码依赖以及持续集成 等实践。当时新版的搜索引擎和云计算平台都在开发中,搜索引擎依赖云计算平台。搜索引 擎团队有几个开发 leader 经常投诉云计算平台,因为他们发现很多次持续集成不通过的原 因,是云计算平台有 bug、不稳定。他们觉得太影响效率,要求云计算平台发布稳定分支, 开发团队基于稳定分支做集成。只有 2 个前 Google 的开发 leader 说:“没关系,我们就依 赖最新的源码,这样才能第一时间帮助他们发现问题,尽快解决,系统就会做的更好。” 携程技术沙龙敏捷专场 496 大家可以看到,开放的企业文化会造就同样开放和有大局观的员工——不 own 一个系统, 工程师们会 own 所有。 这个案例是公司级的极致的敏捷实践。打造这样的工程体系和文化,成本是巨大的。我们要 不要投入这样的成本去换取极致的速度与灵活性?或者说什么时候才是“合适”去做这件事 的时候?我们仍然在探寻中。 携程技术沙龙敏捷专场 497 携程火车票每年 N 倍增长背后,有哪些创新的管理 方法 [作者简介]杨春勤,携程高级项目经理。目前负责携程黄埔训练营,致力于通过 OK 制和 OKR 的培训传播,释放团队创业激情、提升产品研发效率。本文来自于杨春勤在“携程技 术沙龙——敏捷总动员”上的分享。 引言 携程集团内火车票 SBU 这四年来创新创业高速发展形成地上交通业务群,这里面有哪些创 新的管理方法,如何高效驱动业务增长,如何在发展壮大的同时保持高效避免大公司病, 如何激活团队的创业激情,如何培养和管理创业型人才,如何塑造创业文化和避免稀释…… 如果你的组织遇到类似问题,我们的系列实践或许能给你参考。 携程技术沙龙敏捷专场 498 一、创新的组织结构:OK 制 创业团队通常都是小团队很敏捷,随着业务发展团队壮大,大公司病就来了。常见的病症 有如下几种: 1、基层是螺丝钉,中层是监工,高层做决策:基层的角色只是执行,中层上传下达监控基 层有没有认真干活不偷懒,高层是组织驱动力的来源、创新的来源和决策的中枢。这种结 构基层很无聊、中层很烦、高层是瓶颈很累,组织走的很慢。 2、产品部开发部互相博弈,沟通效率低,合作成本高。两边通常有各自的目标,遇到点事 携程技术沙龙敏捷专场 499 就吵。例如技术改造这点事,产品吐槽开发没事找事耽误事,开发鄙视产品什么都不懂。 甚至产品和开发吵不清楚,升级到两边经理吵;两边经理吵不清楚,升级到两边总监吵…… 3、随着业务的发展,人越招越多,当业务进入成熟期,没什么大需求了,这么多人没事干 怎么办?产品不断提 CR 修修补补,开发不断重构翻代码,ROI 是不考虑的,人不要闲着才 是重要的——这种情况看起来大家做的事情都有价值,其实价值很小,甚至无法覆盖人力 成本。 OK 制是携程火车票创业过程中探索创立的组织制度,能在组织发展壮大的同时保持高 效,避免以上各种大公司病。 OK 制在整个组织表现为一个个 OK 组,OK 组模式上和 Scrum Team 有同有异。OK 组一样 有三个角色:O 代表产品经理,K 代表开发经理,加上能做独立交付的跨职能团队。OK 组 没有 Scrum Master 这个角色,因为 OK 组是纯粹自我驱动自组织自管理的。 另外 OK 组的角色是创业团队,O 是小 CEO,K 是小 CTO,O 和 K 共同决策,不同意见以 O 为主。不需要上级总监或老板来下达作战指令或审批决策。中层很少,主要角色是教 练,而非管理监控者。高层除了教练主要是投资人角色,做投资组合决策——往各个 OK 组投多少资源。高层和中层会对 OK 组的决策进行 challenge/教练式提问,为 OK 组决策提 供补充信息和思路,促进 OK 组提升决策质量,但不会代替 OK 组做决策。 OK 组一般能独立算账,升职加薪发奖金首先看 OK 组的业绩,其次才是组内不同人员对业 绩的贡献,所以组内不同职能都具有高度一致的利益,更易精诚合作把蛋糕做大。同时自 主决策比等高层决策能更敏捷适应市场和行业环境变化。 携程技术沙龙敏捷专场 500 上图是 OK 组划分的一个示例。创业早期只有一个 OK 组,随着业务发展团队壮大,为了 保持高效,业务拆分模块、目标分解指标、大团队裂变成多个 OK 组,OK 组的人员规模保 持跟 ScrumTeam 规模相当,几个人顶多十几个。 OK 组是随着产品生命周期动态演化的,如上图所示。  红色代表产品启动期(从 0 到 1),从资源池抽取几个人成立 OK 组搭建和验证产品。  黄色代表产品建设期(从 1 到 N),根据产品路线图推进,从资源池卷进越来越多资源 到 OK 组。  绿色代表产品运营期(从 N 到 N+1),释放 OK 资源,合并公共资源。 携程技术沙龙敏捷专场 501  右边白色代表产品收尾期,及时合并/关闭低产出项目,释放公用资源。 如果公共资源池变得富余,则指示我们需要开拓新业务或突破老业务瓶颈形成新的增长 点,驱动大家持续创新。 上图是两个 OK 组随产品路线演化人员配置的示例。OK 组的人员规模敏捷适应业务的发展 变迁,避免常见的人员只进不出最后人浮于事。 二、高效的运营管理:OKR 携程技术沙龙敏捷专场 502 OKR 是互联网行业时新的目标管理方法,也是高效的沟通工具。其思路源于德鲁克的目标 管理,德鲁克的粉丝 Intel 的总裁 Andy Grove 发明并推行了 OKR,Intel 的 John Doerr 将 OKR 带给了 Google,随着 Google 的成功实施,OKR 方法被其他知名 IT 企业借鉴。 常见目标管理的乱局有两类: 1、缺乏目标的混乱:  尤其是研发部门,有的研发部门没有自己的目标,产品说做啥就做啥。或者团队成员 没有目标,等待领导指令,结果领导很辛苦、员工很悠闲。  有些产品也只有粗略的方向没有有效的目标,杂七杂八的需求做了一大堆,耗费很多 资源但看不到突出成效。  不知道公司战略和部门目标造成方向性浪费。例如产品决定后续停止 online 站点的运 营,研发不知道,还在安排 online 技术改造或自动化测试开发。 2、KPI 管理的弊端:  一切为了 KPI 和绩效奖金,造成低承诺高实现,甚至不择手段。例如为了代码行的 KPI 制造冗余垃圾代码。  不同职能 KPI 对立导致争执冲突内耗。例如开发以 bug 数为代码质量 KPI,测试以找 出 bug 数为效率 KPI,开发和测试为了一个问题是不是 bug 吵个不停。  只见树木不见森林,为了 KPI 忘了初心。有个笑话很应景:一个人沿路不断挖坑,后 面跟着一个人不断填坑,这个奇怪的事情背后是中间种树那个人请假了,这种情况完 成挖坑填坑的 KPI 而不顾种树的初心目标只是浪费资源。 导入 OKR 能有效管理目标,高效驱动业务增长。下面是两个 OKR 的示例。 携程技术沙龙敏捷专场 503 参考示例,我们来看看 OKR 的最佳实践原则: 1、个体级原则 O 和 KR 要聚焦,个人 own 的 O 一般一两个,部门负责人可能 own 三四个 O,只有聚焦才 易突破。每个 O 的 KR 一般也不超过四个,抓住实现 O 的关键点。 O 和 KR 要可衡量。这点跟 SMART 原则是一致的。“提升支付成功率”这样只算方向不算 目标,是不可接受的。 O 要有挑战,KR 要有逻辑和创新。OKR 鼓励和驱动大家突破自己的能力、突破业务的瓶 颈,跳起来才可能实现的 O 才是合格的 O。通过 KR 的逻辑和创新,支撑挑战性 O 的实 现。 OKR 可以演化。这个世界变化越来越快,年初制定的 KPI 年末很可能因为环境变化完全不 适用,甚至从季初到季末都存在刻舟求剑的问题,所以 OKR 可以随着环境的变化或信息的 变化而演化,可能提高目标也可能降低,但原则都是保持目标的适度挑战性。 OKR 要自主评分。通过评分来促进进展盘点和沟通,评分标准有多严谨结果高一点低一点 不重要。 2、组织级原则 OKR 要从 MTP(Massive Transformative Purpose/宏大变革目标)/大目标出发,主动设 定,上下共识。OKR 不像 KPI 那样由上级分解指派,OKR 主要靠自己设定,以上级的 OKR 作为方向性指引,上下沟通达成共识。这样在聚焦的同时激活集体创新。 OKR 要透明。每个人的 OKR 在组织内是公开的,这样容易达成互相了解和齐心合力避免冲 突。 OKR 不挂钩绩效考核。OKR 基于自我驱动自我实现,鼓励挑战,挂钩绩效考核会扼杀这些 内在动力,所以不能以 OKR 的评分作为绩效考核的评分。绩效评估应另做设计。但是 OKR 可以作为沟通工具高效阐述业绩成果和逻辑创新(经常看到有绩效自评列冗长难读的流水 账或“认真负责团结合作”的水话)。 携程技术沙龙敏捷专场 504 以 OKR 为核心的运营管理机制参考下图,每个圆圈都是迭代循环。 外层 MTP 迭代。一般是老板塑造的愿景,例如携程火车票,为全国人民买火车票。 往下是年度战略会。一般是总监级阐述年度战略 OKR。 产品会是每两个月一次(根据业务节奏也可能调整到一个月或三个月),OK 经理的短期 OKR 设定和阶段性运营盘点。 内层迭代是两周一次的 OKR Review 会议(根据业务节奏也可能一周一次),Review OKR 的中途进展。 中高层的教练工作主要在 OKR Review 会议和产品会,高层的投资组合决策主要在产品 会。 携程技术沙龙敏捷专场 505 三、创业型激励机制:投名状 如何让员工像老板一样全心全力创新奋斗?这个问题经常遇到的反问是员工如何像老板一 样拿到高额回报。OK 制把 OK 组作为小创业团队来看待,在 OK 组引入创业型激励也是相 得益彰。 携程技术沙龙敏捷专场 506 上图的投名状示例,OK 组成员跟管理层谈定一个周期内的业绩基线 N,同时 OK 组成员投 资 M 元,如果周期末: 业绩没到 N,OK 组员亏掉 M 元 业绩到 N,OK 组员拿回自己投资的 M 元(不赚不亏) 业绩做到 N 的 1.1 倍,OK 组员拿回 2M 元(投 3 万就是回 6 万) 业绩每超基线 10%,投资回报加一倍。 业绩做到 N 的 2 倍,拿回 10M 元(投 3 万就是回 30 万) 实施投名状的项目大家都非常激情投入和锐意创新,结果也都获得了很大的成功和回报。 携程技术沙龙敏捷专场 507 但不是所有项目都适合投名状,高收益高挑战的项目才适合。另外这个挑战还是基于一定 的逻辑分析的,没有依据的浮夸风目标是浪费沟通和管理成本。 OK 组不同的人贡献是不一样的,所以会设计一定的区分。OK 经理作为小团队的 CEO 和 CTO 是必须投资的,不愿投资代表对项目没信心和不愿承担风险,那就需要换领军人。作 为 OK 组员则可投可不投。同时 OK 经理可以投资更多的额度,OK 经理承担更多的风险和 可能获取更丰厚的汇报。组外一般支持人员不投,视作乙方合作管理,我们的实践经验是 太多合作方投资进来容易扰乱职能本分并引起其他项目合作纠纷。 因为有些团队实现投名状,有些不适合,容易造成大团队的利益不平衡,为了避免贫富差 距过大出现问题,尤其是同等技术能力的工程师在业务团队和支持团队收入差距太大,为 此我们设计了一些平衡规则。包括投资额设上限,回报率设上限;实施投名状的团队不占 用 A 等绩效奖金名额,传统项目奖金也留给非投名状团队;同时支持能力强有创业精神愿 意承担风险争取更多回报的人流向创业型项目。 当前互联网公司多实行股票期权激励,几年生效的股票方便将优秀员工跟公司长期绑定, 有时也会发生个人努力和大盘增长相关度难以看见的问题,像携程这样几万人的公司,除 了高层外的单个人 All in 奋斗也难以看到对大盘业绩和股票的影响,所以总会有人停留舒 适区仅仅本分工作等股票生效。投名状机制是股票期权机制的黄金搭档,有效补充短期高 强度激励,让大家像创业合伙人一样投入心力、脑力和体力。 四、人才培养和管理:黄埔训练营 OK 制和 OKR 作为携程地上交通业务群运营管理和核心和特色,和投名状一起驱动着各业 务单元高速增长。随着业务增长和地盘扩大团队也不断壮大及换血,而保持高速增长需要 将优秀的管理理念和实践经验批量复制给团队新血,黄埔训练营应运而生。 携程技术沙龙敏捷专场 508 黄埔训练营的招生主要面向产品经理和开发经理(OK 组的 O 和 K)。当前课程以 OKR 训练 为主,同时涵盖了创业和创新、MTP 和 OK 制的讲授,期望学员通过课程掌握目标驱动和 业务推进的知识和技能、拥有共同的沟通语言和管理方法,以便高效的协同推动各业务线 开拓创新高速增长。 黄埔训练营根据学习发展心理学和培训管理规律独创“多元进阶”训练法。与现在流行的 在线交互英语教学、塑身训练营等一样,黄埔“多元进阶”训练法,通过有效利用线上视 频/微信/PPT 课件/测评、线下受教/观摩/模拟/实战等将学员轻松带入真实训练场景,让学 员在入营前后的创新知识、能力和意愿都有显著提升。 携程技术沙龙敏捷专场 509 黄埔学员不像传统培训那样上完课就了事,需要完成实战项目后进行毕业答辩,答辩通过 才能拿到黄埔总教练签名颁发的毕业证书。上图是两位总教练——携程集团高级副总裁陈 刚先生和副总裁王玉琛先生在黄埔训练营毕业典礼上讲话。 黄埔毕业证书不仅仅是个荣誉,还是关键岗位的资格证(OK 组的领军人)和升级加薪的 资格证(项目做得好但是 OKR 逻辑阐述差被视为通过运气成功难以复制,可以多发奖金但 不能晋升)。 创业型人才不单单是培养出来,大前提是招募自我驱动强的人,而不是仅盯着知识经验能 力。即使能力有限作为 OK 组普通一兵,自我驱动的人不需要设置个经理岗位去监督跟进 每天具体工作,这会省下很多管理成本,而且自我驱动的人的成长性也大。 所以创业型人才的培养和管理,是以招募自驱者为前提,文化和方法论培训为重点,形成 选育用留整个环。 五、文化塑造和改造:管理层理念革新 携程技术沙龙敏捷专场 510 敏捷转型最容易的是流程导入,其次是工程实践,最难的是文化改造。携程地上交通业务 群的文化是基于创新、效率、自驱和信任的价值观的。这也是 OK 制和 OKR 的存活土壤。 组织文化塑造和改造取决于管理层,尤其是老板。火车票团队是地上交通业务群中最早的 创业团队,文化塑造相对容易。后期不断发展不断招人、不断孵化新业务裂变新团队、不 断整合新业务团队,文化的传承和改造需要新团队管理层的锐意创新和变革决心。 创新、效率和自驱的价值很容易得到认可,而管理层能否信任前线团队放手前线自主决策 敏捷应变却不容易,需要放下理性的自负,相信前线更敏锐集思广益胜过诸葛亮,不再事 事决策审批。 携程技术沙龙敏捷专场 511 而从传统复杂层级职能组织结构转型扁平化的 OK 制组织结构,需要中层的意识角色转变 ——从管理者变成教练,更需要高层的魄力:这个过程会有优秀的人离开,也会有优秀的 人留下来,还有更多优秀的人被吸引进来。 导入 OKR 为核心的运营迭代流程是改变最容易的环节。而人才管理革新则是周期最长最需 要耐心的——尤其是资源紧缺时坚持招募标准。而激励机制的革新则需要管理层乐于分享 利益的胸怀和格局。 文化是虚的,不容易看见,但是仍然有办法传播。上图是我们汇编的内刊,不断总结管理 思路和实践,形成文字,人手一本,方便传承组织文化。 OK 制、OKR、投名状、黄埔训练营、管理层理念革新,携程集团内火车票 SBU 这四年来 创新创业高速发展形成地上交通业+务群,以上是我们核心的管理方法。 携程技术沙龙移动开发专场 512 携程技术沙龙移动开发专场 携程技术沙龙移动开发专场 513 MVP 模式在携程酒店的应用和扩展 [作者简介]赵伟麟,2011 年就职于创新工场旗下点心 OS,2014 年加入携程酒店事业部, 从事 Android 研发工作。擅长基于组件的业务架构,系统架构,建模,性能优化和重构, 关注应用系统的扩展性和耦合性,追求简洁的代码。 现场视频:https://v.qq.com/x/page/i0504pqngn1.html 酒店业务部门是携程旅行的几大业务之一,其业务逻辑复杂,业务需求变动快,经过多年 的研发,已经是一个代码规模庞大的工程,如何规范代码,将代码按照其功能进行分类, 将代码写到合适的地方对项目的迭代起着重要的作用。 MVP 模式是目前客户端比较流行的框架模式,携程在很早之前就开始探索使用该模式进行 相关的业务功能开发,以提升代码的规范性和可维护性,积累了一定的经验。本文将探讨 一下该模式在实际工程中的优点和缺陷,并介绍携程面对这些问题时的思考,解决方案以 及在实践经验基础上对该模式的扩展模式 MVCPI。 一、从 MVC 说起 MVC 已经是非常成熟的框架模式,甚至不少人认为它过时陈旧老气,在实践中,很多同事 会抱怨,MVC 会使得代码非常臃肿,尤其是 Controller 很容易变成大杂烩,预期的可维护 性变得很脆弱,由此导致一方面希望有新框架模式可以解决现在的问题,但同时对框架模 式又有些怀疑,新的框架模式是否能真正解决现在的问题?会不会重蹈覆辙?会不会过度 设计?会不会掉进一个更深的坑?总之,这些类似“一朝被蛇咬,十年怕井绳”的担忧显 得不无道理。但不管如何,我们需要仔细耐心的做工作。 1.1、被误解的 MVC 在 MVP 模式逐渐流行之前,不管我们有意识或无意识地,我们使用的就是 MVC 模式。以 Android 为例,我们来看看 MVC 是什么样子。 publicclass HotelActivity extends Activity{ private TextView mNameView; private TextView mAddressView; private TextView mStarView; @Override protected void onCreate(Bundle savedInstanceState){ super.onCreate(savedInstanceState); setContentView(R.layout.activity_main2); mNameView = (TextView) findViewById(R.id.hotel_view); mAddressView = (TextView) findViewById(R.id.address_view); 携程技术沙龙移动开发专场 514 mStarView = (TextView) findViewById(R.id.star_view); HotelModel hotel = HotelLoader.loadHotelById(1000); mHotelNameView.setText(hotel.hotelName); mHotelAddressView.setText(hotel.hotelAdress); mHotelStarView.setText(hotel.hotelStar); } } 上面的代码,概括了 Android MVC 的基本结构,从笔者的经验来看,很多应用都存在这样 的代码风格,也就是大部分人认为的 MVC: Model : Hotel,HotelLoader Controller: HotelActivity View : mHotelNameView mHotelAddressView mHotelStarView 可以试想一下如果这个界面展示的数据非常的多话,MainActivity 必然会变得非常庞大,就 像大部分人所抱怨的那样。诚然,上面的 demo 是 MVC 模式,但是,它仅是从系统框架 的角度来看,如果从应用框架来看,它不是。下面来看一下,从应用框架来看一下 MVC 正确的结构: 1.2、MVC 的正确姿势 应用中的 MVC 应该在系统的 MVC 框架上根据业务的自身的需要进行进一步封装,也就是 说,如果在我们宣称我们是使用 MVC 框架模式的时候,代表我们的主要工作是封装自己 的 MVC 组件。它看起来应该是像下面的风格: publicclass HotelActivity extends Activity{ private HotelView mHotelView; @Override protected void onCreate(Bundle savedInstanceState){ super.onCreate(savedInstanceState); setContentView(R.layout.activity_main2); mHotelView = (HotelView) findViewById(R.id.hotel_view); 携程技术沙龙移动开发专场 515 HotelModel hotel = HotelLoader.loadHotelById(1000); mHotelView.setHotel(hotel); } } 跟之前的代码相比,基本结构是相似的,如下: Model : Hotel,HotelLoader Controller: HotelActivity View : mHotelView 仅仅 View 层发生了变化,这是因为,Model 和 Controller 相对是大家容易理解的概念,在 面临任何一个业务需求的时候,自然就能产生的近乎本能的封装(尽管 Model 的基本封装 大部分工程师都可完成,但不可否认 Model 的设计是至关重要而有难度的);而对 View 的 看法,可能就是“能正确布局和展示就行”。但这正是关键所在:我们需要对界面进行全 方位的封装,包括 View。具体来说,一个真正的 MVC 框架应该具备下面的特点:  数据都由 Model 进行封装  View 绑定业务实体,view.setXXX  Controller 不管理与业务无关的 View 1.3、MVC 模式的问题所在 前面说到,很多人抱怨采用 MVC 模式使得 Controller 变得很臃肿,我相信,Controller 变 得臃肿是事实,但其归结于采用 MVC 模式是不正确的,这个锅不应该由 MVC 来背,因 为,这个论点会导致我们走向错误的方向从而无法发现 MVC 真正的问题所在。为什么这 么说呢,那是因为在本人了解到的很多情况下,大家并没有正确理解 MVC 框架模式,如 采用前文中第一种模式,自然会使得 Controller 臃肿,但是如果采用第二种模式, Controller 的代码和逻辑也会非常清晰,至少不至于如此多的抱怨。因此如果只是想解决 Controller 臃肿的话,MVC 就够了,毋庸质疑。那 MVC 的问题是什么呢?我想只有深刻的 理解了这个问题,我们才有必要考虑是否需要引入新的框架模式,以及避免新的模式中可 能出现的问题。 View 强依赖于 Model 是 MVC 的主要问题。由此导致很多控件都是根据业务定制,从 Android 的角度来看,原本可以由一个通用的 layout 就能实现的控件,由于要绑定实体模 型,现在必须要自定义控件,这导致出现大量不必要的重复代码。因此有必要将 View 和 Model 进行解耦,而 MVP 的主要思想就是解耦 View 和 Model。由此引入 MVP 就显得很 自然。 携程技术沙龙移动开发专场 516 二、Android MVP 2.1、参考实现 Android 官方提供的 MVP 参考实现,大致思想如下: 1、抽象出 IView 接口,规范控件访问方法,而不限 View 具体来源 publicinterface IHotelView{ public TextView getNameView(); public TextView getAddressView(); public TextView getStarView(); } 2、抽象出 IPresenter 接口,定义 IView 和 Model 的绑定接口 publicinterface IHotelPresenter{ public void setView(IHotelView hotelView); public void setData(HotelMotel hotel); } 3、IPresenter 的实现类,实施数据和 IView 的绑定,并负责相关的业务处理 publicclass HotelPresenter implements IHotelPresenter{ private IHotelView hotelView; public void setView(IHotelView hotelView){ this.hotelView = hotelView; } public void setData(HotelModel hotel){ hotelView.getNameView().setText(hotel.hotelName); hotelView.getAddressView().setText(hotel.hotelAddress); hotelView.getStarView().setText(hotel.hotelStart); } } 4、Activity 实现 IView,角色转变为 View,弱化 Controller 的功能 publicclass HotelActivity extends Activity implements IHotelView{ @Override protected void onCreate(Bundle savedInstanceState){ super.onCreate(savedInstanceState); setContentView(R.layout.activity_main2); HotelModel hotel = HotelLoader.loadHotelById(1000); 携程技术沙龙移动开发专场 517 IPresenter presenter = new Presenter(); presenter.setView(this); presenter.setData(hotel); } @Override public TextView getNameView(){ return (TextView)findViewById(R.id.hotel_name_view); } @Override public TextView getAddressView(){ return (TextView)findViewById(R.id.hotel_address_view); } @Override public TextView getStarView(){ return (TextView)findViewById(R.id.hotel_address_view); } } 上述代码,主要的特点可以概括为:  面向接口  View -Model 解耦  Activity 角色转换 就目前了解到的情况来看,很多采用 MVP 模式的应用基本上和 android 参考实现方案差别 不大,说明该模式的应用场景也是很广泛的。 2.2、Android MVP 存在的问题 尽管已经有了大量的应用,但不可否认该模式的还是存在一些问题,这些问题在携程的使 用过程中也得到了体现。比如,上下文丢失问题,生命周期问题,内存泄露问题以及大量 的自定义接口,回调链变长等问题。可以归纳为:  业务复杂时,可能使得 Activity 变成更加复杂,比如要实现 N 个 IView,然后写更多个 模版方法。  业务复杂时,各个角色之间通信会变得很冗长和复杂,回调链过长。  Presenter 处理业务,让业务变得很分散,不能全局掌握业务,很难去回答某个业务究 竟是在哪里处理的。  用 Presenter 替代 Controller 是一个危险的做法,可能出现内存泄漏,生命周期不同 步,上下文丢失等问题。 以下面的这个需求来看几个具体的示例: 携程技术沙龙移动开发专场 518 详情按钮的展示需要服务端下发标记位控制,展示时点击需要请求一个服务,服务返回时 toast 提示用户。 publicclass HotelPresenter{ private IHotelView mHotelView; private Handler handler = new Handler(getMainLooper()); public void setData(HotelModel hotelModel){ View button = mHotelView.getButtonView(); int visibility = hotelModel.showButton ? .VISIBLE :GONE; button.setVisibility(visibility); if (hotelModel.showButton) { button.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v){ sendRequest(); } }); } private void sendRequest(){ new Thread() { public void run(){ Thread.sleep(15*1000); handler.post(new Runnable() { public void run(){ Toast.makeText(???) //Where is Context? } }); } }.start(); } } 上述代码表明,HotelPresenter 可以处理大部分的业务,但是在最后需要使用上下文的时 候,出现了困难,因为脱离了上下文,展示一个 Toast 都不能实现 为了避免这样的尴尬,因此改进方案如下: publicclass HotelPresenter{ private IHotelView mHotelView; private Fragment mFragment; private HotelPresenter(Fragment fragment){ this.mFragment = fragment; } 携程技术沙龙移动开发专场 519 private Handler handler = new Handler(Looper.getMainLooper()); public void setData(HotelModel hotelModel){ View button = mHotelView.getButtonView(); button.setVisibility(hotelModel.showButton ? VISIBLE :GONE); if (hotelModel.showButton) { button.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v){ sendRequest(); } }); } } private void sendRequest(){ new Thread() { public void run(){ Thread.sleep(15*1000); handler.post(new Runnable() { public void run(){ Context context = mFragment.getActivity(); int duration = LENGTH_SHORT; //NullPointerException will occur Toast.makeText(context,"成功”,duration).show(); } }); } }.start(); } } 改进的方案中,考虑到需要使用上下文,因此新增了接口传入 Fragment 作为上下文,在 Presenter 需要时可以使用,但是,由于 Fragment 生命周期会了变化,可能会导致空指针 问题。 于是新的问题又需要解决。主要是两个思路,一个是为 Presenter 增加生命周期方法,在 Fragment 的生命周期方法里调用 Presenter 对应的生命周期函数,但这就让 Presenter 看起 来像 Fragment 的孙子;另外一个就是承认 Presenter 其实不太合适承担 Controller 的职 责,从而提供接口给外部处理;如下: publicclass HotelPresenter{ private IHotelView mHotelView; private Handler handler = new Handler(Looper.getMainLooper()); 携程技术沙龙移动开发专场 520 public void setData(HotelModel hotelModel){ View button = mHotelView.getButtonView(); button.setVisibility(hotelModel.showButton ? VISIBLE :GONE); if (hotelModel.showButton) { button.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v){ if (mCallback != null) { mCallback.onSendButtonClicked(); } }); } } publicinterface Callback{ public void onSendButtonClicked(); } private Callback mCallback; public void setCallback(Callback callback){ mCallack = callback; } } 这个方案很稳定,似乎成为了最佳的选择。但是自定接口和回调始终有那么一点痛。 三、MVP 的扩展模式 MVCPI 由于前面的分析,MVP 参考实现并不是万能的,携程酒店并没有完全采用参考实现方案, 而是结合自身的实践经验思考之后设计出来的扩展方案。我们主要考虑了一下的几个问 题:  如何定义 View 接口?  如何定位 Presenter ?  如何对待 Controller?  如何解决长长的回调链? 通过对上述问题的思考,提出对应的解决方法,规避前面论述的各种问题,形成了携程酒 店的 MVCPI 框架模式,并在多个业务场景运行,取得了较为满意的效果。下面,详细介绍 MVCPI 模式。 3.1、IView 和 Android 参考实现不一样的是,我们并没有采用强类型的接口作为表达 View 的方式, 携程技术沙龙移动开发专场 521 而是采用弱类型的接口来定义 View。具体定义方式如下: publicinterface IView{ //用于展示酒店名称的控件 int NAME_VIEW = R.id.name_view; //用于展示酒店地址的控件 int ADDRESS_VIEW = R.id.address_view; //用于展示酒店星级的控件 int STAR_VIEW = R.id.star_view; //用于展示酒店详情入口的的控件 int DETAIL_BUTTON = R.id.detail_button; } 上面的接口简洁的描述了作为业务控件的 View 需要具备的子控间 ID,并不需要具体的实 现类。因此也不需要 Activity 去实现这个接口,只需要在 layout 中申明这几个 ID 的即可, 极大的简化了代码。 3.2、Presenter 与参考实现的定位不一样,我们认为由 Presenter 取代 Controller 并不是一个好的做法, Presenter 应是 Controller 的补充,主要起到 View 和 Model 解耦和数据绑定的作用,所负 责的控件的上的业务还是有 Controller 决定如何去处理。另外 setView 接受的参数是一般 的 View,而非一个接口类型,内部根据 IView 定义的 ID 去查找子控件。如下: publicclass CtripHotelPresenter{ TextView mNameView; TextView mAddressView; TextView mStarView; Button mDetailButton; public void setView(View view){ mNameView = (TextView)mView.findViewById(IView.NAME_VIEW); mAddressView = (TextView)mView.findViewById(IView.ADDRESS_VIEW); mStarView = (TextView) mView.findViewById(IView.STAR_VIEW); mDetailButton = (Button) mView.findViewById(IView.DETAIL_BUTTON); } public void setData(HotelModel hotel){ mNameView.setText(hotel.hotelName); mAddressView.setText(hotel.hotelAdress); mStarView.setText(hotel.hotelStar); int v = hotel.showButton ? View.VISIBLE : View.GONE; mDetailButton.setVisibility(v); } } 携程技术沙龙移动开发专场 522 3.3、Interactor Interactor 是我们定义出来的扩展元素,在 MVP 和 MVC 中都没有对应的角色。为了阐述它 的含义,我们先来看看两个非常常见的场景。 回调链过长 在前面介绍过,Presenter 自定义接口是很多候选方案中较为合理的选择,但相比 MVC 而 言,MVP 更容易出现如上图的一种调用和回调关系(甚至更长)。维护这种回调链通常来 说是一件非常头痛的事情,从 View 的角度来看,很难知道某个事件到最后究竟完成了什么 业务,Acitivity 也不知道到要装配哪些回调。某个未知的新需求可能需要将该链条上的每 个环节都增加回调。 下面来是另外一种场景,大家可以脑补一下采用上面的回调方案,回调链会是什么情况。 携程技术沙龙移动开发专场 523 交互集中型界面 在该界面有几个特点:  几十种动态交互需求,  分布于不同的模块  分布于不同深度的嵌套层次中 经过大量版本迭代后,无论产品经理,研发或者测试,都不清楚到底有哪些需求,业务逻 辑是什么,写在什么地方等等...... 上述两个场景可以得出两个结论:  排查问题非常耗时  增加功能成本高,容易引致其他问题 为了解决上述两个比较棘手的问题,我们引入了 Interactor,用于描述整个界面的交互,一 举解决上述两个问题。我们认为交互模型是一个功能模块的重要逻辑单元,相对于实体模 型来说,交互模型更加抽象,在大多数的情况,并不能引起大家的注意,但它确实是如实 体一样的存在,正是因为没有对交互进行系统的描述,才导致上面两种突出的问题。尽管 抽象,但是交互模型本质非常简单,它有着和实体模型有相似的结构,示例如下: publicclass HotelOrderDetailListeners{ 携程技术沙龙移动开发专场 524 public View.OnClickListener mBackListener; // 返回按钮点击事件监听者 public View.OnClickListener mShareClickListener;//分享按钮事件监听者 public View.OnClickListener mConsultClickListener;//咨询按钮事件监听者 …… } 通过对界面整体分析后,我们建立如上的交互模型,所有的交互都在交互模型进行注册, 由交互模型统一管理,进而可以对整个界面的交互进行宏观把控;然后在页面的所有元素 中共享同一个交互模型,进而各个元素不再需要自定义接口和避免建立回调链。最后由 Controller 负责组装,进一步加强 Controller 的控制能力。 3.4、MVCPI 全貌 最后,整体介绍一下 MVCPI 的代码结构 1、首先定义整个界面中有哪些用户交互,本例中就一个详情按钮交互 publicclass HotelInteractor{ //点击详情的事件处理器 public View.OnClickListener mDetail; } 2、Presenter 构造时需要传入交互模型,内部定义了 IView 接口,传入的 View 中需要包含 它定义的 ID 的控件,在 bindData 时,详情按钮的点击不是通过匿名内部类去处理,而是 直接引用交互模型中定义的 mDetail publicclass HotelPresenter{ private View hotelView; private HotelInteractor mInteractor; private Button mDetailButton; public HotelPresenter(HotelInteractor interactor){ this.mInteractor = interactor; } privateinterface IView{ int DETAIL= R.id.detail_button; …… } public void setView(View hotelView){ this.hotelView = hotelView; mDetailButton= (Button)findViewById(IView. DETAIL ); } public void setData(HotelModel hotel){ 携程技术沙龙移动开发专场 525 if (hotel.showButton) { mDetailButton.setVisibility(View.Visibile); mDetailButton.setOnClickListener(mInteractor.mDetail); } } } 3、Controller 负责界面各个元素(包括交互模型)的初始化和装配 publicclass HotelActivity extends Activity{ @Override protected void onCreate(Bundle savedInstanceState){ super.onCreate(savedInstanceState); setContentView(R.layout.activity_main2); HotelInteractor interactor = new HotelInteractor(); interactor.mDetail = new View.OnClickListener() { public void onClick(View view){ viewHotelDetail();//处理详情业务; } }; HotelModel model= HotelLoader.loadHotelById(1000); HotelPresenter presenter = new HotelPresenter (interactor); View view= findViewById(R.id.hotel_view); presenter.setView(view); presenter.setData(hotel); } } 四、结论 通过对 MVC,MVP 的介绍和研究,我们发现二者的关系并不是相互取代的关系,而是一 种演化和改进的关系。经实践证明,MVC 仍然具有强大的生命力,试图用 MVP 取代 MVC 几乎都会失败。携程在 MVC 模式基础上,结合 MVP 思想,加入 Interactor 元素搭建的 MVCPI 框架模式,一方面将数据绑定逻辑从 Controller(或者 View)中分离出去,另一方 面将交互模型的控制纳入进来,进一步加强了 Controller 的控制能力。无论从代码的简洁 性,维护性,扩展性来看,都具有较大优势,具有一定的实践推广价值。 当然,任何框架模式都不是全能的,MVCPI 也存在它不足,如果有好的意见和建议,欢迎 加入,一起讨论推进框架模式的发展。 携程技术沙龙移动开发专场 526 携程用户数据采集与分析系统 [作者简介]王小波,携程技术中心框架研发部高级工程师,主要负责用户行为数据采集系 统及相关数据产品研发设计工作。之前主要从事互联网广告、RTB 相关系统研发和设计工 作。 现场视频:https://v.qq.com/x/page/s0504e9bxuv.html 一、携程实时用户数据采集系统设计实践 随着移动互联网的兴起,特别是近年来,智能手机、pad 等移动设备凭借便捷、高效的特 点风靡全球,同时各类 APP 的快速发展进一步降低了移动互联网的接入门槛,越来越多的 网民开始从传统 PC 转移至移动终端上。但传统的基于 PC 网站和访问日志的用户数据采集 系统已经无法满足实时分析用户行为、实时统计流量属性和基于位置服务(LBS)等方面的 需求。 我们针对传统用户数据采集系统在实时性、吞吐量、终端覆盖率等方面的不足,分析了在 移动互联网流量剧增的背景下,用户数据采集系统的需求,研究在多种访问终端和多种网 络类型的场景下,用户数据实时、高效采集的方法,并在此基础上设计和实现实时、有序 和健壮的用户数据采集系统。此系统基于 Java NIO 网络通信框架(Netty)和分布式消息 队列(Kafka)存储框架实现,其具有实时性、高吞吐、通用性好等优点。 1、技术选型和设计方案: 一个典型的数据采集分析统计平台,对数据的处理,主要由如下五个步骤组成: 图 1、数据平台处理流程 其中,数据采集步骤是最核心的问题,数据采集是否丰富、准确和实时,都直接影响整个 数据分析平台的应用的效果。本论文关注的步骤主要在数据采集、数据传输和数据建模存 储这三部分。 为满足数据采集服务实时、高效性、高吞吐量和安全性等方面的要求,同时能借鉴互联网 大数据行业一些优秀开源的解决方案,所以整个系统都将基于 Java 技术栈进行设计和实 现。整个数据采集分析平台系统架构如下图所示: 携程技术沙龙移动开发专场 527 图 2、数据采集分析平台系统架构 其中整个平台系统主要包括以上五部分:客户端数据采集 SDK 以 Http(s)/Tcp/Udp 协议根 据不同的网络环境按一定策略将数据发送到 Mechanic(UBT-Collector)服务器。服务器对采 集的数据进行一系列处理之后将数据异步写入 Hermes(Kafka)分布式消息队列系统。为了关 联业务服务端用户业务操作埋点、日志,业务服务器需要获取由客户端 SDK 统一生成的用 户标识(C-GUID),然后业务服务器将用户业务操作埋点、日志信息以异步方式写入 Hermes(Kafka)队列。最后数据消费分析平台,都从 Hermes(Kafka)中消费采集数据,进行 数据实时或者离线分析。其中 Mechanic(UBT-Collector)系统还包括对采集数据和自身系统 的监控,这些监控信息先写入 Hbase 集群,然后通过 Dashboard 界面进行实时监控。 (1)基于 NIO 的 Netty 网络框架方案 要满足前面提到的高吞吐、高并发和多协议支持等方面的要求。我们调研了几种开源异步 IO 网络服务组件(如 Netty、MINI、xSocket),用它们和 NginxWeb 服务器进行了性能对 比,决定采用 Netty 作为采集服务网络组件。下面对它进行一些概要介绍:Netty 是一个高 性能、异步事件驱动的 NIO 框架,它提供了对 TCP、UDP 和文件传输的支持,Netty 的所 有 IO 操作都是异步非阻塞的,通过 Future-Listener 机制,用户可以方便的主动获取或者 通过通知机制获得 IO 操作结果。 携程技术沙龙移动开发专场 528 图 3、Netty 框架内部组件逻辑结构 Netty 的优点有: a、功能丰富,内置了多种数据编解码功能、支持多种网络协议。 b、高性能,通过与其它主流 NIO 网络框架对比,它的综合性能最佳。 c、可扩展性好,可通过它提供的 ChannelHandler 组件对网络通信方面进行灵活扩展。 d、易用性,API 使用简单。 e、经过了许多商业应用的考验,在互联网、网络游戏、大数据、电信软件等众多行业得到 成功商用。 Netty 采用了典型的三层网络架构进行设计,逻辑架构图如下: 图 4、Netty 三层网络逻辑架构 第一层:Reactor 通信调度层。该层的主要职责就是监听网络的连接和读写操作,负责将网 络层的数据读取到内存缓冲区中,然后触发各种网络事件,例如连接创建、连接激活、读 事件、写事件等,将这些事件触发到 Pipeline 中,再由 Pipeline 充当的职责链来进行后续 的处理。 第二层:职责链 Pipeline 层。负责事件在职责链中有序的向前(后)传播,同时负责动态 的编排职责链。Pipeline 可以选择监听和处理自己关心的事件。 第三层:业务逻辑处理层,一般可分为两类:a. 纯粹的业务逻辑处理,例如日志、订单处 理。b. 应用层协议管理,例如 HTTP(S)协议、FTP 协议等。 携程技术沙龙移动开发专场 529 我们都知道影响网络服务通信性能的主要因素有:网络 I/O 模型、线程(进程)调度模型 和数据序列化方式。 在网络 I/O 模型方面,Netty 采用基于非阻塞 I/O 的实现,底层依赖的是 JDKNIO 框架的 Selector。 在线程调度模型方面,Netty 采用 Reactor 线程模型。常用的 Reactor 线程模型有三种,分 别是: a、Reactor 单线程模型:Reactor 单线程模型,指的是所有的 I/O 操作都在同一个 NIO 线 程上面完成。对于一些小容量应用场景,可以使用单线程模型。 b、Reactor 多线程模型:Rector 多线程模型与单线程模型最大的区别就是有一组 NIO 线程 处理 I/O 操作。主要用于高并发、大业务量场景。 c、主从 Reactor 多线程模型:主从 Reactor 线程模型的特点是服务端用于接收客户端连接 的不再是一个单独的 NIO 线程,而是一个独立的 NIO 线程池。利用主从 NIO 线程模型, 可以解决一个服务端监听线程无法有效处理所有客户端连接的性能不足问题。Netty 线程 模型并非固定不变的,它可以支持三种 Reactor 线程模型。 在数据序列化方面,影响序列化性能的主要因素有: a、序列化后的码流大小(网络带宽占用)。 b、序列化和反序列化操作的性能(CPU 资源占用)。 c、并发调用时的性能表现:稳定性、线性增长等。 Netty 默认提供了对 GoogleProtobuf 二进制序列化框架的支持,但通过扩展 Netty 的编解 码接口,可以实现其它的高性能序列化框架,例如 Avro、Thrift 的压缩二进制编解码框 架。 通过对 Netty 网络框架的分析研究以及对比测试(见后面的可行性分析测试报告)可判 断,基于 Netty 的数据采集方案能解决高数据吞吐量和数据实时收集的难点。 (2)客户端数据加解密和压缩方案 对一些明感的采集数据,需要在数据传输过程中进行加密处理。目前存在的问题是,客户 端采集代码比较容易被匿名用户获取并反编译(例如 Android、JavaScript),导致数据加密 的算法和密钥被用户窃取,较难保证数据的安全性。根据加密结果是否可以被解密,算法 可以分为可逆加密和不可逆加密(单向加密)。具体的分类结构如下: 携程技术沙龙移动开发专场 530 图 5、加密算法分类 密钥:对于可逆加密,密钥是加密解算法中的一个参数,对称加密对应的加解密密钥是相 同的;非对称加密对应的密钥分为公钥和私钥,公钥用于加密,私钥用于解密。私钥是不 公开不传送的,仅仅由通信双方持有保留;而公钥是可以公开传送的。非对称密钥还提供 一种功能,即数字签名。通过私钥进行签名,公钥进行认证,达到身份认证的目的。 根据数据采集客户端的特点,对于采集数据使用对称加密算法是很明智的选择,关键是要 保证对称密钥的安全性。目前考虑的方案主要有: a、将加解密密钥放入 APP 中某些编译好的 so 文件中,如果是 JavaScript 采集的话,构造 一个用 C 编写的算法用于生成密钥,然后借助 Emscripten 把 C 代码转化为 JavaScript 代 码,这种方案有较好的混淆作用,让窃听者不太容易获取到对称密钥。 b、将密钥保存到服务器端,每次发送数据前,通过 HTTPS 的方式获取加密密钥,然后对 采集数据进行加密和发送。 c、客户端和服务器端保存一份公钥,客户端生成一个对称密钥 K(具有随机性和时效 性),使用公钥加密客户端通信认证内容(UID+K),并发送到服务器端,服务端收到通信 认证请求,使用私钥进行解密,获取到 UID 和对称密钥 K,后面每次采集的数据都用客户 端内存中的 K 进行加密,服务器端根据 UID 找到对应的对称密钥 K,进行数据解密。 这三种客户端数据加密方式基本能解决客户端采集数据传输的安全性难题。 采集数据压缩。为了节省流量和带宽,高效发送客户端采集的数据,需要使用快速且高压 缩比的压缩算法,目前考虑使用标准的 GZIP 和定制的 LZ77 算法。 (3)基于携程分布式消息中间件 Hermes 的数据存储方案 Hermes 是基于开源的消息中间件 Kafka 且由携程自主设计研发。整体架构如图: 携程技术沙龙移动开发专场 531 图 6、Hermes 消息队列整体架构 Hermes 消息队列存储有三种类型: a、MySQL 适用于消息量中等及以下,对消息治理有较高要求的场景。 b、Kafka 适用于消息量大的场景。 c、Broker 分布式文件存储(扩展 Kafka、定制存储功能)。 由于数据采集服务的消息量非常大,所以采集数据需要存储到 Kafka 中。Kafka 是一种分布 式的,基于发布/订阅的消息系统。它能满足采集服务高吞吐量、高并发和实时数据分析的 要求。它有如下优秀的特性: a、以时间复杂度为 O(1)的方式提供消息持久化能力,即使对 TB 级以上数据也能保证常数 时间复杂度的访问性能。 b、高吞吐率。即使在非常廉价的商用机器上也能做到单机支持每秒 100K 条以上消息的传 输。 c、支持 Kafka Server 间的消息分区,及分布式消费,同时保证每个 Partition 内的消息顺序 传输。 d、同时支持离线数据处理和实时数据处理。 e、Scale out,即支持在线水平扩展。 一个典型的 Kafka 集群中包含若干 Producer(可以是 Web 前端产生的采集数据,或者是服 务器日志,系统 CPU、Memory 等),若干 broker(Kafka 支持水平扩展,一般 broker 数量 越多,集群吞吐率越高),若干 ConsumerGroup,以及一 Zookeeper 集群。Kafka 通过 Zookeeper 管理集群配置,选举 leader,以及在 Consumer Group 发生变化时进行 rebalance。Producer 使用 push 模式将消息发布到 broker,Consumer 使用 pull 模式从 broker 订阅并消费消息。Kafka 拓扑结构图如下: 携程技术沙龙移动开发专场 532 图 7、Kafka 拓扑结构 我们知道,客户端用户数据的有序性采集和存储对后面的数据消费和分析非常的重要,但 是在一个分布式环境下,要保证消息的有序性是非常困难的,而 Kafka 消息队列虽然不能 保证消息的全局有序性,但能保证每一个 Partition 内的消息是有序的。在用户数据采集和 分析的系统中,我们主要关注的是同一个用户的数据是否能保证有序,如果我们在数据采 集服务端能将同一个用户的数据存储到 Kafka 的同一个 Partition 中,那么就能保证同一个 用户的数据是有序的,因此基本上能解决采集数据的有序性。 (4)基于 Avro 格式的数据灾备存储方案 当出现网络严重中断或者 Hermes(Kafka)消息队列故障情况下,用户数据需要进行灾备存 储,目前考虑的方案是基于 Avro 格式的本地文件存储。其中 Avro 是一个数据序列化反序 列化框架,它可以将数据结构或对象转化成便于存储或传输的格式,Avro 设计之初就用来 支持数据密集型应用,适合于远程或本地大规模数据的存储和交换。 Avro 定义了一个简单的对象容器文件格式。一个文件对应一个模式,所有存储在文件中的 对象都是根据模式写入的。对象按照块进行存储,在块之间采用了同步记号,块可以采用 压缩的方式存储。一个文件由两部分组成:文件头和一个或者多个文件数据块。其存储结 构如下图所示: 携程技术沙龙移动开发专场 533 图 8、Avro 对象容器文件格式 灾备存储处理过程是:当网络异常或者 Hermes(Kafka)消息队列出现故障时,将采集的用户 数据解析并转化成 Avro 格式后,直接序列化存储到本地磁盘文件中,数据按 Kafka-Topic 分成多个文件存储,且每小时自动生成一个新的文件。当网络或者 Hermes(Kafka)故障恢复 后,后端线程自动读取磁盘 Avro 文件,将数据写入 Hermes(Kafka)消息队列的对应 Topic 和分区中。每个文件写入成功后,自动删除灾备存储文件。这样能增加用户数据采集服务 的健壮性和增强服务容错性。 2、架构设计方案可行性分析 在相同配置的测试服务器上(包括数据采集服务器、Hermes(Kafka)集群)做如下对比实验 测试:(使用 ApacheBenchmark 进行 Web 性能压力测试工具) (1)Netty VS Nginx 处理网络请求对比 在不对采集数据进行业务处理的情况下(即只接请求并做响应,不做业务处理,也不存储 采集数据),在 5000 并发,Keepalive 模式下均能达到每秒处理 4 万多请求,其中 Nginx 的 CPU、内存消耗会小一些。测试对比数据如下:(ab 参数: -k –n 10000000 –c 5000) 携程技术沙龙移动开发专场 534 (2)Netty 对采集数据进行业务处理 Netty 服务加上采集数据解析相关业务处理,以及处理后的数据写入 Hermes(Kafka)消息队 列。可以进行简单的间接估算。如果采集服务要求达到:每秒处理 3 万左右请求,99%的 请求完成时间小于 800ms 的目标,则采集数据解析和存储流程的处理时间必须在 600ms 以内。而这两步又分为数据解析和数据存储,可以分别进行压力测试加以验证。根据我们 的压力测试,采集数据解析和存储也能完全满足性能要求。 经以上对比实验测试表明,使用 Netty 服务组件收集、解析数据并直接写入 Hermes(Kafka) 分布式消息队列的方案初步具备可行性。 二、相关数据分析产品介绍 基于实时采集到的用户数据和系统监控数据,我们开发了一套相关的数据分析产品。产品 的内容主要分以下几部分:(1)、 API 和页面性能报表;(2)、页面访问和流量;(3)、用户 行为分析;(4)、系统异常崩溃分析;(5)、数据实时查询工具;(6)、采集数据排障工 具;(7)、其它。其中详细分类如下图所示: 携程技术沙龙移动开发专场 535 图 9、数据分析产品分类 现选取其中几个比较常见的产品做下简单介绍: 1、单用户浏览跟踪 作用:实时跟踪用户浏览记录,帮助产品优化页面访问流程、帮助用户排障定位问题。 使用案例:根据用户在客户端上的唯一标识 ID,如:手机号、Email、注册用户名、 ClientId、VisitorId 等查询此用户在某一时间段顺序浏览过的页面和每个页面的访问时间及 页面停留时长等信息。如果用户在浏览页面过程中发生了异常崩溃退出情况,可以结合应 用崩溃信息关联查询到相关信息。 2、页面转化率 作用:实时查看各个页面的访问量和转化情况,帮助分析页面用户体验以及页面布局问 题。 使用案例:用户首先配置页面浏览路径,如 p1023-> p1201 -> p1137 -> p1300,然后根 据用户配置页面浏览路径查询某个时间段各个页面的转化率情况。如有 1.4 万用户进入 p1023 页面,下一步有 1400 用户进入下一页面 p1201。这样可推算出页面 p1201 的转化率 为 10%左右。这是最简单的一种页面转化率,还有间接的页面转化率,即只匹配第一个和 最后一个页面的访问量。同时可以按各种维度进行条件筛选,比如:网络、运营商、国 家、地区、城市、设备、操作系统等等。 携程技术沙龙移动开发专场 536 3、用户访问流 作用:了解每个页面的相对用户量、各个页面间的相对流量和退出率、了解各维度下页面 的相对流量。 使用案例:用户选择查询维度和时间段进行查询,就能获取到应用从第一个页面到第 N 个 页面的访问路径中,每个页面的访问量和独立用户会话数、每个页面的用户流向、每个页 面的用户流失量等信息。 4、点击热力图 作用:发现用户经常点击的模块或者区域,判断用户喜好、分析页面中哪些区域或者模块 有较高的有效点击数、应用于 A/B 测试,比较不同页面的点击分布情况、帮助改进页面交 互和用户体验。 使用案例:点击热力图查看工具包括 Web 和 APP 端,统计的指标包括:原始点击数(当 前选中元素的原始点击总数)、页面浏览点击数(当前选中元素的有效点击数,同一次页面 浏览,多次点击累计算 1 次点击)、独立访客点击数(当前选中元素的有效点击数,同一用 户,多次点击累计算 1 次点击)。 5、采集数据验证测试 作用:快速测试是否能正常采集数据、数据量是否正常、采集的数据是否满足需求等。 使用案例:用户使用携程 APP 扫描工具页面的二维码,获取用户标识信息,之后正常使用 携程 APP 过程中,能实时地将采集到的数据分类展示在工具页面中,对数据进行对比测试 验证。 6、系统性能报表 作用:监控系统各业务服务调用性能(如 SOA 服务、RPC 调用等)、页面加载性能、APP 启动时间、LBS 定位服务、Native-Crash 占比、JavaScript 错误占比等。按小时统计各服务 调用耗时、成功率、调用次数等报表信息。 基于前端多平台(包括 iOS、Android、Web、Hybrid、RN、小程序)数据采集 SDK 的丰 富的自动化埋点数据,我们可以对数据、用户、系统三方面进行多维度立体的分析。服务 于系统产品和用户体验、用户留存、转换率及吸引新用户。 携程技术沙龙移动开发专场 537 去哪儿网快速 App 开发及问题解决平台实践 [作者简介]张子天,去哪儿网平台事业部客户端技术总监。2011 年加入去哪儿网,曾担任 无线机票 Android 总监、无线架构总监,目前主要负责 Qunar 客户端基础架构和客户端 生态建设。 现场视频:https://v.qq.com/x/page/j0504v2z4gn.html 在大规模客户端开发过程中,大家经常会遇到各种层出不穷的稀奇古怪的问题,而往往问 题也隐藏的比较深,不容易去发现和迅速解决。长期被各类问题困扰,是身为工程师的我 们,不能忍受的。所以我们要想办法解放自己,解放生产力。 一、用户场景 首先我们考虑一个经常面对的场景。 这是一个非常常见的问题,小白用户,或者是说普通的用户,往往是没有这个能力去理解 App 是在什么状态下发生了一个什么问题的,甚至有时候是闪退还是其他的问题都是有可 能搞不清楚。这次的情景中,我们的用户能提供的信息只有 2 个。 携程技术沙龙移动开发专场 538 然而我们需要知道的信息有 用户闪退时间 闪退具体页面 闪退的原因 在没有我们的问题细查的情况下,能够想到的最快的办法就是查崩溃日志,然后根据崩溃 的信息去看到底用户是怎么崩溃的。然后再联系相关的负责人员去看怎么解决问题,然而 现实永远是残酷的。 现阶段的客户端开发涉及到方方面面的角色,有 native,有 hybrid,有 react-native,甚至 携程技术沙龙移动开发专场 539 某些场景是多种开发方式混合的,不同的业务也有不同的负责人,同一个团队也同时面向 多个 App 产品做产出,所以当我们去定位问题的时候会一个个把各个负责人都拉到群里去 讨论,排除,协助查日志。 最终我们会在深更半夜叫醒了一群无辜的小伙伴,效率低下的处理了一个可能也不是非常 严重的故障。 二、如何获得新生 携程技术沙龙移动开发专场 540 同样是这样的一个问题,我们只需要还原用户的使用轨迹,根据用户的交互和网络请求, 往往一两个人就可以找到用户遇到的问题究竟是哪里产生的,再也不用追着用户问题他到 底在哪里遇到了问题,怎么能复现这些用户可能都无法回答的问题。 1、 技术细节 我们能够完成这样一个用户场景的还原,所有数据来自三个独立系统: 无埋点统计 性能监控 异常监控 携程技术沙龙移动开发专场 541 那么问题就来了: 交互日志如何收集 如何做网络监控 如何将不同维度的数据串联 2、QAV_无埋点交互统计平台 做业务开发的伙伴们一定都遇到过这样的一个状况,feature 开发的时间往往比做埋点统计 之类的时间还要短一些,每次的业务迭代都会伴随着大量的业务不相干的逻辑处理,久而 久之,对业务代码的可读性和合理重构产生了非常大的阻碍,而且还有一个致命的问题, 一旦需要统计老版本的 App 的某些数据,我们就束手无策了。那么基于工程师精神 (lan),我们就在想能不能在不影响业务开发的情况下,把这些数据统计类的事情完成了 呢? 携程技术沙龙移动开发专场 542 首先,我们可以看到要监控的事件(上图)。 其次,不论是什么平台,交互事件需要控件的唯一标识。 关于唯一标识,实际上我们也走了很长的路。 最开始的时候,我们采用的是 view 的 id 去作为一个唯一标识,这个方案的问题在于 view 的 id 相对于开发者来讲,是有意义的,然而这个意义不一定是和产品角度保持的一致,当 App 迭代的过程中,不可避免的修改了 id 的名字的时候,就无法进行对应,而且会有很多 的 view 并没有 id 的情况出现,种种原因,view 的 id 做唯一标识方案逐渐被淘汰。 携程技术沙龙移动开发专场 543 另一个方案是用坐标来做标识,这个方案原本是应用在 iOS 场景上的,但由于后期 iPhone 出不同的尺寸的屏幕,故而坐标的方案需要大量的转换和对应,也没有办法做的非常灵 活,所以这个方案也逐渐淘汰掉。 最后我们采用了更为优质的 XPath 的方式来进行锁定唯一标识,XPath 的优势在于可以定 义各种平台的场景下的唯一控件,并且将上述几种标识方式的缺点弥补。 值得一提的是,这里在 Android 上面要处理不同的厂商的 ROM 下,root 布局不一致的问 题,在 iOS 我们还根据某个 view 在 parent 中的坐标排序进行了稳定性的定位,以保证同 一个控件尽可能的少的被误判成多个控件。 既然提到了是无埋点,那么不干扰业务的操作就十分的重要了。 携程技术沙龙移动开发专场 544 我们通过 AOP 的手段去做这些监控数据的提取和处理,保证不打扰真正的业务代码的编 写。 3、性能监控 我们从用户的行为说到了网络请求,那么同样的思路,我们需要用 AOP 的手段去把网络部 分的监控数据获取到。 这里不得不提到的是在 Android 上和 iOS 上有着不同的实际情况,iOS 上情况比较简单, 由于系统提供了可以 Runtime 期 Hook 的 API,我们可以很方便的用替换插桩的形式注入 携程技术沙龙移动开发专场 545 我们的代码: 而在 Android 上虽然同样有类似 instant-run 的方案可以做 Runtime 的 AOP,但在 dex 的 格式上注定不能真正的做到 Runtime,只是预先安放了大量的空方法段,以备不时只需, 但这样的方案有比较严重的性能浪费,故而我们并不想用这个方案去解决我们的问题。 Compile 期的 AOP 才是解决问题的根本途径。 制作使用插件,利用 javeagent 原理,hook 住 jvm 进程,在构建的 class 转 dex 阶段,运 用 ASM 框架,根据特定规则去转化或生成特定类,进而达到代码注入的功效。 携程技术沙龙移动开发专场 546 Gradle 插件。 1、在 dex 任务前插入自定义任务 installInject,利用 tools.jar(jdk 中,自行拷贝)中 VirtualMachine,attach 到当前运行的 pid,并且 loadAgent 指定的 agentJar 并传递参数 inject=true 和其他参数,这样 agentJar 就会在 main 方法前启动。 2、在 assemble 任务后插入自定义任务 B,重新 loadAgent,传递参数 inject=false,不再 Hook 进程进行类的转化。 为什么最后要执行 uninstallInject 任务,因为所有通过 ProcessBuilder 启动的 jvm 都已经携 带上 agentJar 了,为了不影响后续 JVM 的正常使用,必须使 agentJar 停止工作。 三、数据聚合 异常监控的内容不是本次分享的重点,不展开去详细描述。大家可以发现,在这个过程 中,各个环节涉及到了 3 个不同的系统,那么如何将数据做好整合,也是一个要解决的难 点。 首先是日志的上传机制,交互日志/网络请求日志经过压缩打包,在不同的场景下触发上 传;崩溃或卡顿等异常日志则为实时上传。上传的数据包中会有本地事件的时间戳,用于 后续的数据对齐。 那么如何串联数据呢,我们在每一个交互产生的时候,生成一个 requestId,在每次有网络 请求时,将这个 requestId 注入到网络请求中去。完成用户操作和网络请求的一对多的绑 定。另外我们还为每次请求做一个 uuid,在请求链路中会像 TCP 的报文包一样,逐层携带 一个头信息,保证后续的请求链路的串联。 上面提到每个数据都有一个本地的时间戳,这里的时间戳会和上传日志的时间做一个校正 携程技术沙龙移动开发专场 547 差值,获得相对于 Server 的一个稳定的时间,而对于不同的数据来说,本地时间戳又可以 保证数据顺序在同一时间系中的一致性。 至此,我们就将用户的操作场景可以做到一个还原,呈现出用户的时间线可以非常有效的 解决未知因素较多的棘手问题,提高效率,减小沟通成本。 然而实际上问题细查只是整套系统的一个组成部分,是整个大的筋斗云中不同的系统之间 的一个有机结合。整个筋斗云提供的是 App 完整的从开发到测试到运营等等完整的支撑, 我们将 App 和业务进行了剥离,使得整个环节中可以复用的程度从代码级提升到了组件和 系统级。 携程技术沙龙移动开发专场 548 携程技术沙龙移动开发专场 549 更多的内容我们将来会有更多的分享呈现,有大家感兴趣的方面欢迎共同探讨。 携程技术沙龙测试专场 550 携程技术沙龙测试专场 携程技术沙龙测试专场 551 去哪儿自动化测试框架 Qunit 中的零侵入切面技术 应用及分布式运行平台 [作者简介]毛京超,任职去哪儿网酒店事业部,负责代理商对接业务线相关的测试工作, 参与去哪儿 Qunit 自动化测试框架的开发。 蒋承君,去哪儿网金融事业部测试工程师,负责金融事业部主系统的测试工作及测试工具 研发。本文来自其在“携程技术沙龙——移动互联背景下的测试技术创新”上的分享。 一、Qunit 简介 Qunit 是去哪儿网基于 Junit 框架自主研发的接口自动化测试框架,目前支持的被测接口协 议类型包括:HTTP 接口、Dubbo RPC 接口和 Hessian 接口。 该自动化测试框架将常用功能的代码实现(测试数据准备、远程执行 SQL、调用被测接口 等)封装成一个个标签,测试人员编写自动化测试用例时,只需要按照测试步骤进行规范 格式 XML 文件编写,不必关心具体功能代码的实现,将更多的精力放到自动化测试用例的 设计上。 同时 Qunit 自动化测试框架对接口响应的断言也进行完美的封装,通过将接口响应与基线 数据(之前录制的接口响应数据)进行 diff 的方式进行自动断言,大大提高了自动化测试 用例编写的效率。 本次分享的内容是:Qunit 自动化测试框架中针对 Mock 第三方接口数据开发的零侵入切面 技术的应用模块和加速自动化用例测试执行速度开发的分布式运行平台模块。 二、零侵入切面技术的应用 1、遇到的问题 大家在编写接口自动化测试用例时必然会涉及到 Mock 第三方接口数据,遇到以下几个问 题应该是家常便饭:  第三方接口数据结构复杂,需要通过查看接口文档、日志和实现代码等手段进行拼接  被测试接口响应结果对第三方接口的数据有很强的依赖,我们编写一个接口的自动化 用例需要准备好多份 Mock 数据用来支持  一个第三方接口影响多个接口的逻辑,测试时修改 Mock 数据后,这个自动化接口跑 成功了,另一个接口的测试用例没法执行了,需要不断修改 Mock 数据地址进行测试 以上问题的存在,增加了自动化测试用例的编写的时间成本,影响自动化测试用例的编写 效率。那么有没有一种方式可以动态的更改第三方数据呢? 携程技术沙龙测试专场 552 下面介绍 Qunit 自动化测试框架如何引入零侵入切面技术的应用模块来解决这个问题的。 2、解决方案 JavaAgent 是拥有修改应用运行代码的一个软件组件。在 agent 的上下文中, instrumentation 提供了重新定义和修改装载在运行时的类(class)的能力。 Qunit 自动化化框架基于该技术开发了 Catcher agent 模块,通过修改 CLASS 字节码文件实 现动态录制和回放第三方接口数据的功能,服务于自动化测试。 该技术方案最大的优点就是不需要对被测系统进行代码修改,即完全无代码入侵的方式实 现了对被测系统和第三方模块或构件交互的监视和 mock 功能。 Catcher agent 模块的相关代码也不需要布置到线上环境,和线上生产环境是绝对隔离的, 不会造成任何影响。 Catcheragent 怎么实现了对第三方接口数据的录制和回放功能呢?下面通过 Catcher agent 修改 CLASS 字节码前后的代码对比进行介绍。 修改字节码前: publicReturnType method(ParamsType[] params) throws Exception{ //something } 修改字节码后: privateReturnType $catcher$method(ParamsType[] params) throws Exception{ //something } publicReturnType method(ParamsType[] params) throws Exception{ ReturnType returnObj; try{ if(needMock(className, methodName,params)){ returnObj = mock(className,methodName, params); }else{ returnObj =$catcher$method(params); } }finally{ if(needCollect(className, methodName)){ collect(className, methodName,params, returnObj); } } return returnObj; 携程技术沙龙测试专场 553 } 通过对比可以看出以下几点:  Catcher Agent 将被测试代码进行重新封装增加相关代码实现  通过 needMock 函数控制当前应用是使用 Mock 数据,还是调用真实的接口;  通过 needCollect 函数控制是否对数据进行录制  需要注意的是 collect 是异步存储不会对程序运行造成影响 Qunit 自动化框架中零侵入切面技术的应用包括录制模式和回放模式两个模式:  录制模式:录制第三方数据,将第三方报文数据保存到本地,用来编写自动化用例使 用,可以对录制下来的数据进行参数化配置;  回放模式:使用本地准备好的 Mock 数据对第三方接口进行 Mock,支撑自动化测试。 录制模式: 回放模式: 携程技术沙龙测试专场 554 进行回放模式时,大多数情况下都会对本地存储的第三方测试数据进行参数化,更灵活的 应用录制下来的测试数据,因此测试执行时,会先将本地存储的第三方数据和测试用例中 配置的变量参数组织成一份完整的测试数据发送给 Catcher Agent 进行 mock 第三方接口。 3、Qunit 中使用例子 在 service 中定义 catcher 切点 自动化测试用例中的使用方法 携程技术沙龙测试专场 555 {"respCode":"00","respMsg":"成功"} 执行测试 TestCatcher 被测接口时,对 test-fetchPost 第三方接口进行 Mock,执行测试 时,第三方接口的 mock 数据为通过传入 json 格式的赋值,设置 respCode=“00”, respMsg=“成功” 录制的测试数据 <#noparse> { "respCode":"${respCode}", "currencyCode":"156", "validation":"true", "encoding":"UTF-8", "bizType":"000301", "respMsg":"${respMsg}", "orderId":"170308161349237045589", "accessType":"0" } 录制后的测试数据对 respCode 和 respMsg 进行参数化,Qunit 编写自动化测试用例时,可 以通过 json 的数据格式对参数化的字段进行重新赋值,使得 mock 数据使用更灵活。 三、分布式运行平台 1、遇到的问题 随着 Qunit 自动化测试框架逐步完善,所能支撑的自动化测试场景更加全面,同学们感受 到了自动化测试带来的福利,就扩大了自动化测试用例的覆盖,自动化测试用例的场景设 计的也越来越复杂,测试用例数量随之暴增,随之测试执行时间的问题就暴露了,原来一 次全量用例执行需要 10 分钟,后来每次自动化测试需要坐等 1 个小时,测试执行成本增 加了。这就是去哪儿网某事业部的使用 Qunit 自动化框架时遇到的问题。 Qunit 的测试执行方式继承了 Junit 的测试执行方式,通过执行 mvn test 命令进行单线程执 行的,试想如果可以多个线程并行执行测试用例,6 个线程并行执行测试,那么 10 分钟就 是执行完毕,测试执行时间成本不就可以降低很多吗? 携程技术沙龙测试专场 556 针对这个问题,我们开发了分布式运行平台模块进行并行执行 Qunit 的自动化测试用例。 2、解决方案 要并行执行自动化测试用例,需要解决以下问题:  并行执行使用什么策略进行分配测试用例?  并行执行测试用例,测试结果怎么收集到一起?  多个测试用例并行执行,如果测试用例之间有相互影响怎么办? 使用过 Junit 的同学都知道,Junit 的执行原理是先将所有待执行的测试用例加载到内存 中,再逐个循环进行执行,最终汇总测试结果生成测试报告。Qunit 的执行原理也是这样 的,那么我们是否可以对 Qunit 循环执行测试用例的逻辑进行重写,使其按照我们指定的 测试文件进行执行测试呢?是否可以每执行一个测试用例后,就将测试结果实时发送到一 个平台中,让平台对其进行汇总展示呢?测试用例之间的相互影响是否可以通过多套独立 的测试环境进行解决呢? 在去哪儿网做 QA 是幸福的,因为公司有个稳定 Noah 环境管理平台,可以按照自己定义 的测试环境模板,动态创建多套独立的测试环境(包括部署被测应用所需的机器、数据 库、memeched、redis 等),每套测试环境相互对立。 分布式运行平台通过调用 Noah 环境管理平台的接口创建多套独立的测试环境,按照测试 用例文件维度分发测试用例到不同的测试环境中进行执行,并且分发策略参考了每个测试 用例文件上次执行时间的长短,优先执行消耗时间最长的测试用例文件,进一步缩短整体 测试执行的时间。 下图为分布式运行平台的执行自动化测试用例的流程 携程技术沙龙测试专场 557 分布式运行平台做为去哪儿网统一执行 Qunit 自动化测试用例的平台,还做调度模板管 理、调度任务管理、测试环境管理、测试报告展示、代码覆盖率统计、通过接口调用创建 测试任务等功能,用来更友好、更高效的支撑 Qunit 自动化测试用例执行。 四、总结 零侵入切面技术是使用 java agent 的技术进行开发,基于这个技术点我们还开发了 Catcher 系统,可以支持 java 工程的任何一个类的方法的返回值进行录制和回放,目前已经在功能 测试进行试用。 分布式运行平台的核心功能是通过创建多套环境并行执行自动化测试用例及汇总测试结果 的方式,达到缩短整体测试执行时间的目的,该平台除支撑 Qunit 自动化测试用例的执行 外,后续会支撑去哪儿网其他自动化测试框架的测试执行,最终成为一个公司级通用的分 布式运行平台。 携程技术沙龙测试专场 558 携程酒店 360 度 Java 质量控制 [作者简介]王幸福,携程酒店研发部资深测试开发工程师,负责酒店测试框架和测试工具 的研发。技术狂热者,热衷于开源项目,利用创新去提高测试工作的效率。 现场视频:https://v.qq.com/x/page/q0522xiqued.html 一、前言 携程目前很多的框架和项目都在往 Java 技术栈上进行迁移。在这个过程中我们遇到很多的 挑战和困难,为此酒店测试在原有的测试体系的基础上做了大量的工作,构建了一整套卓 有成效的质量保障体系。所以,在本文的开始部分会给大家介绍下目前酒店测试体系的一 些情况,后面则会详细的介绍下这个体系的一部分-Java 覆盖率统计平台。 二、何为 360 度质量保障体系 我们常见的测试流程一般如下图所示,功能测试,自动化测试等这些测试阶段和行为都是 围绕着被测系统进行,所以我们可以形象的把它们的关系看作一个 360 度的环,而被测系 统则被围在了环的中央,就像被保镖保护起来的重要人物一般。 那很容易想到的是,这个环上的保镖越多,围得越密,那么被保护的人当然就越安全。可 是,保镖也是需要成本的,如果被保护的人不是那么的重要,当然也就用不了这么多的保 安。所以,根据被测系统的重要性以及成本的考量,不同的公司对质量保障体系有着不同 考量。 携程技术沙龙测试专场 559 常见的测试保障体系 携程酒店测试的质量保障体系在传统的质量体系中增加了一些 “保镖”,不同的是,其中 一部分增加的“保镖”是机器人。这样既增加了被测系统的安全性,也适当的降低了成 本。无疑携程酒店的 360 度质量保障体系的核心就是自动化。也只要有这样,无论是持续 集成,API 测试以及监控预警,利用自动化都达到了质量和效率的双重保障。 携程酒店 360 度质量保障体系 1. 单元测试 单元测试作为代码级别的质量保障手段,有其不可替代的作用。虽然,携程酒店的敏捷开 发中并没有强制进行 TDD 或 BDD 这类的实践。但作为自动化测试之外有利的补充,也是 要求对于自动化测试或者手工测试无法有效测试的部分,需要编写单元测试用例进行测 试。 2. 持续集成 目前酒店测试自动化平台和携程发布系统进行整合,每次应用在发布系统中的发布,自动 化测试平台都会进行测试用例的执行,并发送测试报告给测试人员。测试人员收到报告后 会对失败的用例进行分析,如果有问题就记入 Bug,如果是用例本身的问题,则修改测试 用例。目前酒店测试持续集成包含了 API,UI 以及 Job 这几种自动化测试,且除了 UI 自动 化之外都实现了无码测试用例的编写,测试人员可以很便捷的编写和维护相应的测试用例 3. 集成测试 在此阶段,测试人员主要进行的是功能测试,为了给测试人员工作提供便利,我们构建了 三个平台: Compass,测试管理平台,测试人员在此平台可以及时了解自己的工作情况,比如本周的 携程技术沙龙测试专场 560 任务有哪些?各种自动化测试的执行情况如何等等。 CAS,测试自动化平台,测试人员可以根据需要手动的去触发执行自动化测试用例,并得 到详尽的报告。 Click,测试工具平台,测试人员在整个测试周期中肯定会用到各种各样的工具,而在 Click 中测试人员可以很快捷的找到并使用自己需要的工具。 4. 回归测试 在回归测试中,持续集成依然会继续进行,而且通过在早期对测试用例执行已经进行过分 析,此时测试用例的质量已经得到了加强。测试自动化的实施效果应该会更显著。 5. 性能测试 我们提供了两种性能测试方式,场景简单的性能测试,测试人员可以通过性能测试平台自 助的完成性能测试,而对于场景复杂的性能测试,测试人员可以在性能测试平台中申请常 规性能测试,由专业的性能测试人员完成性能测试。 6. 监控预警 产品上线的时候,大家都是如履薄冰,为了能尽早尽快的发现发布后的问题,及时快速的 定位问题,我们开发了监控预警平台,其中包括日志预警,性能预警,机器预警以及报表 监控。 三、Java 覆盖率统计平台 1. 为什么要做代码覆盖率 前面我们介绍酒店目前的质量保障体系,那么大家可能会注意到,在整个测试周期内会产 生大量的测试用例,单元测试用例,API 测试用例,UI 测试用例,Job 测试用例,功能测 试用例等等。那么就面临着一个问题:如何量化这些测试用例的质量,如何衡量测试的完 整度和有效性。自然而然的,我们想到了覆盖率,覆盖率是度量测试完整性的一个手段, 是测试有效性的一个度量,覆盖率有两种评测方法:基于需求的覆盖率和基于代码的覆盖 率。 基于需求的覆盖率比较的直观,被测系统一共有多少功能,我们编写的测试用例,测试了 多少功能,一目了然,所以平常我们测试最多使用的是基于需求覆盖的方式,但是基于需 求覆盖的方式很大程度上依赖于需求文档的完整性,测试人员的设计测试用例的水平,覆 盖的完整度差异还是比较大的。 基于代码的覆盖率虽然看起来就不那么直观,你很难一眼就看出来覆盖的代码对应的是什 么功能,但是它却比需求覆盖率更为实在,你覆盖了哪些代码,没覆盖哪些代码都是比较 清楚的,可以得到一个量化的数据。 携程技术沙龙测试专场 561 需求覆盖率和代码覆盖率是一个相辅相成的关系,在执行测试用例后,可以通过代码覆盖 率了解自己还有哪些功能没覆盖,补充测试用例后,代码覆盖率自然也会提高。通过代码 覆盖率去完善测试用例是代码覆盖率的重要作用之一。 2. 常见代码覆盖率统计方法 在开发覆盖率统计平台之前,我们也尝试过不同的覆盖率统计的方法,但是都不太能满足 我们的需求。 常见代码覆盖率统计方法的不足 3. Java 覆盖率统计平台简介 在设计 Java 覆盖率统计平台之初,我们就设定了以下几个目标: 使用简单便捷 支持测试各个阶段的代码覆盖率统计 与自动化测试进行集成 与现有的发布和测试流程进行集成 覆盖率统计数据要易于查看 针对设定的这些目标,我们对现有的发布系统,自动化测试平台,Jacoco,Sonar,Gitlab 进行了整合。基本的思路就是利用 Jacoco 的 jacocoagent 获取到覆盖率统计的 exec 文件, 在服务器上对覆盖率统计文件利用 Sonar 进行分析,进而我们再通过 Sonar 的接口获取到 具体的覆盖率统计的数据。 携程技术沙龙测试专场 562 Java 覆盖率统计平台的网络部署图 Java 覆盖率统计平台架构图 4. Java 覆盖率统计平台功能介绍 1)统计测试各个阶段的代码覆盖率 从单元测试到系统测试,整个测试生命周期内都可以进行代码覆盖率的统计。 携程技术沙龙测试专场 563 2)代码覆盖率黑白名单设置 在很多情况下,我们可能只需要统计某一部分代码的覆盖率情况。Java 覆盖率平台提供了 黑白名单设置功能来实现该功能。 3)静态代码扫描 因为平台整合了 Sonar,所以也支持代码扫描功能。使用 Sonar 扫描,可以检查 开发代码中潜在的缺陷和不良的编码习惯。 4)一键统计 覆盖率平台与我们现有的自动化测试平台进行了整合,我们在开启覆盖率统计后,调用自 动化测试平台的接口进行测试用例的执行,测试用例执行完毕后进行覆盖率分析,最后得 到覆盖率统计报告。 5)覆盖率统计数据查看 覆盖率统计完毕后,可以通过在 Sonar 中进行代码覆盖率数据的查看。我们也会通过 Sonar 的 Api 把覆盖率数据落地到服务器的数据库中。这样我们就可以知道每次覆盖率统 计的数据,进而进行覆盖率数据深入的分析。 6)定时任务设置 用户也可以通过设置定时任务,设置某个时刻执行哪些应用的覆盖率统计,在定时任务执 行完毕后,用户会得到覆盖率统计数据的报告。 四、尾声 携程酒店的 360 度质量保障体系依然在演化着,朝着更全面,更智能,更效率的方向在努 力。在这个提倡数据化、智能化、国际化的互联网时代,传统的测试实践已经在经受着考 验。如何能在这些挑战面前保障软件的质量,如何能利用创新来提高效率和质量,这是摆 携程技术沙龙测试专场 564 在所有测试人面前的问题。 携程技术沙龙测试专场 565 携程机票无线测试技术与效能提升 [作者简介]罗昭君,携程机票无线高级测试经理,负责机票移动端功能测试、自动化测 试、平台开发等。从事开发、测试工作近 12 年,先后在阿里巴巴、携程任职。 现场视频:https://v.qq.com/x/page/l0522eg6km3.html 一、敏捷下移动测试痛点 当前在互联网特别是移动端的快速发展下,企业间的竞争日益激烈,绝大部分企业研发体 系都转变为业务、产品驱动模式,研发流程为了适应快速响应、快速迭代,大多也都采用 敏捷的模式来进行管理。 1、敏捷 在产品+开发+测试进行螺旋式迭代的研发中,要求快速跟进竞品,新功能快速上线试错, 有些时候上线时间是根据业务方的需求而定,这样工作排期往往是倒推制定的,测试阶段 很可能会被压缩。 加之敏捷研发下需求任务拆分较细,研发过程中可能会临时塞入一些紧急需求,负责这些 需求的产品经理可能都会以任务紧急同时任务量不大的理由来说服开发和测试人员接受。 这样对于整体质量和回归测试范围都会带来一定的风险。 2、移动测试 移动端的测试对象一般包含:服务端、Android 客户端、iOS 客户端、H5、Hybrid、RN 等 系统,同时面临着多机型、多版本的情况,一个点的改动涉及则是多面的。因此开发小范 围的改动也可能造成大范围的影响,在复杂业务和高耦合的系统架构情况下就会更为明 显。 在有限的测试周期内,如何评估测试范围,用尽可能少的测试用例去覆盖尽可能多的业务 场景则是考验测试人员的关键点,即使如此,效率和质量也还是需要平衡的关键。 携程技术沙龙测试专场 566 严格意义上说,只要是需要划定测试范围而不是进行全范围的全量测试,都属于基于风险 的测试,只是风险可控还是不可控的问题。基于风险的测试必然导致基于风险的发布,因 此,灰度也是为质量风险做的一项兜底工作。这一点在追求速度,快速占领市场和用户的 互联网企业尤为明显。 因此,在敏捷下移动端的测试,如何在研发全流程阶段去暴露风险和问题,进行全程的质 量保证,而不是将风险全部积压在测试阶段,就显得更加重要。 二、测试前置 测试前置也可称为测试左移,它并不是单纯意义上的让测试人员和测试工作更早的介入。 而是建立一整套全流程质保和全员质保的理念,这些工作更多的是需要测试人员去驱动和 引导。 其中涉及到的工作大致可分为 PRD 静态测试、服务契约测试、代码静态扫描、单元测试、 服务接口测试、开发自测、测试准入检查、入测质量数据统计等。 携程技术沙龙测试专场 567 测试前置的工作初期可能会造成测试和开发人员的工作量加大,但是如果流程逐步顺畅、 产品设计质量和代码质量逐步提升,进入良性循环后,研发整体的效率和过程中的工时浪 费将会得到明显的改善。 推进测试前置工作中需要把握几个重点: 1)作为测试团队 leader,需要得到上级领导的认可与支持,制定出可行方案由上而下推 行,并且得到平行的开发团队和产品团队的认同与配合。 2)产品、开发、测试等角色中涉及到测试前置的工作,需要评估入工作量,例如 scrum 工作模式下,sprint 中每个 story 涉及到的 UT、开发自测、产品自测、测试验收等工作耗 时都需要计入工作量,否则即使全员有质量意识但在高强度的工作下也可能有心无力。当 然前期可能对 scrum team 的任务吞吐量造成一些影响,但是从长远看是有利于质量和效率 的提升的。 3)产品的 PRD 和开发人员的代码在结构上均需要优化可测试性。产品文档主要是在格式 和逻辑梳理上方便进行场景分类测试。 4)技术层面上需要为测试前置提供有力的框架和工具支持,例如低门槛高效率的自动化测 试框架、持续交付、持续集成平台、方便开发产品自测的测试数据构造工具等等。 5)测试人员在这个过程需要扮演串联各个环节和监督、总结的角色(当然 Scrum Master 也可以承担其中一些工作)。每个 sprint 结束回归会议上,测试人员需要提供每个节点各个 角色的工作数据,例如代码静态扫描的问题及改善、UT 的覆盖率、开发的自测覆盖率、自 测通过率、入测准时率等等。 这些客观数据用于鼓励团队成员持续改善我们的前置测试工作,并且也是帮助发现 sprint 过程中的问题点。这项工作非常重要,数据的客观和准确需要得到每一位成员的认同,只 有这样才能降这项工作长久持续的坚持并且优化好。 例如,以下是我们一个日常发布的一次开发过程数据: 携程技术沙龙测试专场 568 三、自动化测试&测试自动化 1、分层测试 前面提到移动端属于系统结构中的最末端,其后台的支持系统庞大复杂,调用关系多、系 统架构分层多。因此,我们的测试工作也是需要根据系统结构进行很好的分层、隔离测 试。 例如,下图是携程机票移动端大致的一个调用结构图: 互联网应用的分层测试一般情况下可以分成以下测试层面: 携程技术沙龙测试专场 569 以下是携程机票分层测试的比重分布的目标,其中单元测试的工作由开发人员完成是效率 较高的选择,测试人员可以承担代码静态扫描和 UT 覆盖率的统计、改善跟进工作。 其中 UT 部分,前期大家追求行覆盖率,到一定程度后则需要更重视代码分支覆盖。后期 在尽可能增长分支覆盖率的阶段,测试人员可以参与到 UT 的 Test Case 扩充的工作中。 携程技术沙龙测试专场 570 2、服务接口测试 接口自动化技术相对成熟,在基本的框架层面上并没有太多的技术难度问题,主要将公司 服务各种协议契约的序列化、报文发送接收解析处理类封装好,加上数据驱动和基本工具 类,基于 Nunit 即可将接口测试代码基本跑起来。 但是接口测试真正要做好产生收益,首先必须要深入业务做到足够的覆盖面和测试深度, 其次项目运行的通过率要足够高,以避免测试过程中的人为干预和排错的成本过高以及降 低测试结果的权威性。因此,需要在这几个方面重点做工作:  基于业务场景的动态测试数据  接口依赖  多接口串联测试  环境稳定性、性能  基于测试数据的校验方法 这里重点提一下动态数据,我们的接口自动化失败的 test case 很多情况下并不是 bug,而 是发生了未期望的异常情况,其中有一部分都是由于测试数据问题造成的。 因此我们的数据驱动的数据很大程度上不应该是静态的,而是需要是符合现有测试环境的 动态数据。不管是通过 mock 还是做 Data Provider,都要保证测试接口的数据需要既符合 测试场景又能自动化的做动态调整。 下图是携程机票的测试数据模板,其中数据的模板只涉及请求和响应报文的契约结构(可 携程技术沙龙测试专场 571 以自动生成),具体的数据填充则由其他接口在代码中动态填充。 3、UI 自动化测试 Appium 在 1.6.3 开始对 IOS 新的自动化测试工具 XCUITest 进行支持,相比较 IOS 早先使 用 UIautomation 测试,在控件识别的效率和稳定性上都有明显的提升。 外需要使用 Facebook 提供的开源 WebDriverAgent 来操控手机与 Server 通信,但是这个 WDA 在 inspector 和获取 page resource 方面还不足够稳定和完善,目前在支持 Native 和 Hybrid 的切换上也还不够便利。大家在实际使用时也可以尝试 Macaca 团队开发的 XCTestWD。 UI 自动化测试工作的落地最大的挑战并不是单纯的技术问题,而是如何能够贴近业务利用 技术手段来解决实施过程中的阻碍。 UI 自动化的目标是提高测试效率以加快迭代速度、降低资源成本、覆盖更大测试范围等, 携程技术沙龙测试专场 572 但是 UI 自动化的特点就是维护和调试成本高,稳定性比起底层自动化差,mobile 相对于 PC 端则更为明显。 因此涉及到的核心问题就是 ROI,很多团队无法将 UI 自动测试一直坚持做下去主要的原因 也是 ROI 无法达到预期。所以需要我们思考如何降低 UI 自动化的实施成本,其中包括开发 成本和运行成本。 UI 自动化需要重点关注的领域:  降低框架使用门槛(关键字驱动、录制、自然语言解析等…)  框架和平台的 debug、排错功能  脚本跨平台、跨机型、跨版本的复用性  基于场景的动态测试数据  分布式执行平台(测试设备管理、Test case 动态分配、执行、错误重试、远程调试、 日志管理…)  后台服务依赖 Mock 四、自动化测试配套 携程技术沙龙测试专场 573 为了解决上文中提到的自动化过程中动态测试数据、系统依赖、环境稳定、业务校验、脚 本执行效率等问题,我们在自动化周边建设配套设施,主要包括提供:  动态测试数据工厂:主要包括测试数据的自动化构造、数据库数据后台自动轮询、已 使用数据还原  Mock 平台:人工配置接口报文、自动化配置报文、生产流量报文导入等  测试校验:可配置化断言、业务报文比对、页面图像比对  持续集成环境:自动化构建、测试用例分发、分布式执行、远程调试 在 mock 平台的建设上,需要把握以下重点: 携程技术沙龙测试专场 574  调用方相互隔离  支持多种协议报文  上下文多接口串联 mock  生产流量配置报文  支持场景化配置报文  跨团队的报文配置共享 五、精细化模糊测试 模糊测试是用自动化或者半自动化的方式,采用大量随机的数据输入,来测试系统的响应 逻辑的一种测试技术方法。我们提出的精细化模糊测试,就是将大量的随机测试输入进行 场景细分,以便于我们能够在测试过程中根据场景需要进行细分测试。 携程机票团队进行精细化的模糊测试,主要是依靠 mock 平台为中心来设置测试输入数 据、利用比对工具的方式来进行结果校验。 携程技术沙龙测试专场 575 具体方法:  系统代码中预先根据场景埋入对于标签  Mock 平台通过标签拉取生产环境报文  Mock 平台根据场景建立测试用例填入生产报文  Mock 作为统一数据源接入两套被测系统测试环境  批量执行测试用例调用两套测试环境  将待测代码的响应结果与基准代码的响应结果对比 携程技术沙龙测试专场 576 携程技术沙龙测试专场 577 六、小结 综上所述,在敏捷研发模式下,测试基于风险测试同时要兼顾质量和效率的双保障,那么 自自动化测试等技术的应用则是势在必行的。 自动化测试并非单一的技术个体,它分布于系统架构的各个层面,也融入于白盒测试、黑 盒测试、灰盒测试等多种测试方式中,更重要的是它需要全方位的配套体系的支持,包含 且不仅局限于测试前的测试数据、测试用例的自动化构造、测试环境的自动化搭建、后台 依赖的隔离,测试中的自动化运行管理、个性化的校验方式,测试后的数据还原、恢复、 测试结果聚合报告、排错等等。 这些都属于测试自动化体系中的多个重要环节,每个环境协同配合好才能将自动化测试工 作顺畅、健壮、低成本的运行起来,只有这样,我们才能做到不是为了自动化测试而自动 化测试。 携程技术沙龙架构专场 578 携程技术沙龙架构专场 携程技术沙龙架构专场 579 携程开源配置中心 Apollo 的设计与实现 [作者简介]宋顺,携程框架研发部技术专家。2016 年初加入携程,主要负责中间件产品的 相关研发工作。毕业于复旦大学软件工程系,曾就职于大众点评,担任后台系统技术负责 人。 现场视频:https://v.qq.com/x/page/w0533msi4bo.html 一、What is Apollo 1.1 背景 随着程序功能的日益复杂,程序的配置日益增多:各种功能的开关、参数的配置、服务器 的地址…… 对程序配置的期望值也越来越高:配置修改后实时生效,灰度发布,分环境、分集群管理 配置,完善的权限、审核机制…… 在这样的大环境下,传统的通过配置文件、数据库等方式已经越来越无法满足开发人员对 配置管理的需求。 Apollo 配置中心应运而生! 1.2 Apollo 简介 Apollo(阿波罗)是携程框架部门研发的开源配置管理中心,能够集中化管理应用不同环 境、不同集群的配置,配置修改后能够实时推送到应用端,并且具备规范的权限、流程治 理等特性。 Apollo 支持 4 个维度管理 Key-Value 格式的配置: 1)application(应用) 2)environment(环境) 3)cluster(集群) 4)namespace(命名空间) 同时,Apollo 基于开源模式开发,开源地址:https://github.com/ctripcorp/apollo 1.3 配置基本概念 既然 Apollo 定位于配置中心,那么在这里有必要先简单介绍一下什么是配置。 携程技术沙龙架构专场 580 按照我们的理解,配置有以下几个属性: 配置是独立于程序的只读变量 1)配置首先是独立于程序的,同一份程序在不同的配置下会有不同的行为 2)其次,配置对于程序是只读的,程序通过读取配置来改变自己的行为,但是程序不应该 去改变配置 3)常见的配置有:DB Connection Str、Thread Pool Size、Buffer Size、Request Timeout、 Feature Switch、Server Urls 等 配置伴随应用的整个生命周期 配置贯穿于应用的整个生命周期,应用在启动时通过读取配置来初始化,在运行时根据配 置调整行为 配置可以有多种加载方式 配置也有很多种加载方式,常见的有程序内部 hard code,配置文件,环境变量,启动参 数,基于数据库等 配置需要治理 1)权限控制 由于配置能改变程序的行为,不正确的配置甚至能引起灾难,所以对配置的修改必须有比 较完善的权限控制 2)不同环境、集群配置管理 同一份程序在不同的环境(开发,测试,生产)、不同的集群(如不同的数据中心)经常需 要有不同的配置,所以需要有完善的环境、集群配置管理 3)框架类组件配置管理 还有一类比较特殊的配置 - 框架类组件配置,比如 CAT 客户端的配置。 虽然这类框架类组件是由其他团队开发、维护,但是运行时是在业务实际应用内的,所以 本质上可以认为框架类组件也是应用的一部分 这类组件对应的配置也需要有比较完善的管理方式 二、Why Apollo 正是基于配置的特殊性,所以 Apollo 从设计之初就立志于成为一个有治理能力的配置管理 携程技术沙龙架构专场 581 平台,目前提供了以下的特性: 统一管理不同环境、不同集群的配置 1)Apollo 提供了一个统一界面集中式管理不同环境(environment)、不同集群 (cluster)、不同命名空间(namespace)的配置 2)同一份代码部署在不同的集群,可以有不同的配置,比如 zk 的地址等 3)通过命名空间(namespace)可以很方便的支持多个不同应用共享同一份配置,同时还 允许应用对共享的配置进行覆盖 配置修改实时生效(热发布) 用户在 Apollo 修改完配置并发布后,客户端能实时(1 秒)接收到最新的配置,并通知到 应用程序 版本发布管理 所有的配置发布都有版本概念,从而可以方便地支持配置的回滚 灰度发布 支持配置的灰度发布,比如点了发布后,只对部分应用实例生效,等观察一段时间没问题 后再推给所有应用实例 权限管理、发布审核、操作审计 1)应用和配置的管理都有完善的权限管理机制,对配置的管理还分为了编辑和发布两个环 节,从而减少人为的错误 2)所有的操作都有审计日志,可以方便的追踪问题 客户端配置信息监控 可以在界面上方便地看到配置在被哪些实例使用 提供 Java 和.Net 原生客户端 1)提供了 Java 和.Net 的原生客户端,方便应用集成 2)支持 Spring Placeholder, Annotation 和 Spring Boot 的 ConfigurationProperties,方便应 用使用(需要 Spring3.1.1+) 携程技术沙龙架构专场 582 3)同时提供了 Http 接口,非 Java 和.Net 应用也可以方便的使用 提供开放平台 API 1)Apollo 自身提供了比较完善的统一配置管理界面,支持多环境、多数据中心配置管 理、权限、流程治理等特性 2)不过 Apollo 出于通用性考虑,对配置的修改不会做过多限制,只要符合基本的格式就 能够保存 3)在我们的调研中发现,对于有些使用方,它们的配置可能会有比较复杂的格式,而且对 输入的值也需要进行校验后方可保存,如检查数据库、用户名和密码是否匹配 4)对于这类应用,Apollo 支持应用方通过开放接口在 Apollo 进行配置的修改和发布,并 且具备完善的授权和权限控制 部署简单 1)配置中心作为基础服务,可用性要求非常高,这就要求 Apollo 对外部依赖尽可能地少 2)目前唯一的外部依赖是 MySQL,所以部署非常简单,只要安装好 Java 和 MySQL 就可 以让 Apollo 跑起来 3)Apollo 还提供了打包脚本,一键就可以生成所有需要的安装包,并且支持自定义运行 时参数 三、Apollo at a glance 3.1 基础模型 如下即是 Apollo 的基础模型: 1)用户在配置中心对配置进行修改并发布 2)配置中心通知 Apollo 客户端有配置更新 3)Apollo 客户端从配置中心拉取最新的配置、更新本地配置并通知到应用 携程技术沙龙架构专场 583 3.2 界面概览 上图是 Apollo 配置中心中一个项目的配置首页  在页面左上方的环境列表模块展示了所有的环境和集群,用户可以随时切换  页面中央展示了两个 namespace(application 和 FX.apollo)的配置信息,默认按照表格 模式展示、编辑。用户也可以切换到文本模式,以文件形式查看、编辑 携程技术沙龙架构专场 584  页面上可以方便地进行发布、回滚、灰度、授权、查看更改历史和发布历史等操作 3.3 添加/修改配置项 用户可以通过配置中心界面方便的添加/修改配置项: 输入配置信息: 3.4 发布配置 通过配置中心发布配置: 携程技术沙龙架构专场 585 填写发布信息: 3.5 客户端获取配置(Java API 样例) 配置发布后,就能在客户端获取到了,以 Java API 方式为例,获取配置的示例代码如下: 3.6 客户端监听配置变化(Java API 样例) 通过上述获取配置代码,应用就能实时获取到最新的配置了。 不过在某些场景下,应用还需要在配置变化时获得通知,比如数据库连接的切换等,所以 Apollo 还提供了监听配置变化的功能,Java 示例如下: 携程技术沙龙架构专场 586 3.7 Spring 集成样例 Apollo 和 Spring 也可以很方便地集成,只需要标注@EnableApolloConfig 后就可以通过 @Value 获取配置信息: 四、Apollo in depth 通过上面的介绍,相信大家已经对 Apollo 有了一个初步的了解,接下来我们深入了解一下 Apollo 的核心概念和背后的设计。 4.1 Core Concepts application (应用) 携程技术沙龙架构专场 587 1)这个很好理解,就是实际使用配置的应用,Apollo 客户端在运行时需要知道当前应用 是谁,从而可以去获取对应的配置 2)每个应用都需要有唯一的身份标识 - appId,我们认为应用身份是跟着代码走的,所以 需要在代码中配置: Java 客户端通过 classpath:/META-INF/app.properties 来指定 appId .Net 客户端通过 app.config 来指定 appId environment (环境) 1)配置对应的环境,Apollo 客户端在运行时需要知道当前应用处于哪个环境,从而可以 去获取应用的配置 2)我们认为环境和代码无关,同一份代码部署在不同的环境就应该能够获取到不同环境的 配置 3)所以环境默认是通过读取机器上的配置(server.properties 中的 env 属性)指定的,不 过为了开发方便,我们也支持运行时通过 System Property 等指定,server.properties 文件 路径如下: Windows:C:\opt\settings\server.properties Linux/Mac:/opt/settings/server.properties cluster (集群) 1)一个应用下不同实例的分组,比如典型的可以按照数据中心分,把上海机房的应用实例 分为一个集群,把北京机房的应用实例分为另一个集群。 2)对不同的 cluster,同一个配置可以有不一样的值,如 zookeeper 地址。 3)集群默认是通过读取机器上的配置(server.properties 中的 idc 属性)指定的,不过也 支持运行时通过 System Property 指定 namespace (命名空间) 1)一个应用下不同配置的分组,可以简单地把 namespace 类比为文件,不同类型的配置 存放在不同的文件中,如数据库配置文件,rpc 配置文件,应用自身的配置文件等 2)应用可以直接读取到公共组件的配置 namespace,如 DAL,RPC 等 3)应用也可以通过继承公共组件的配置 namespace 来对公共组件的配置做调整,如 DAL 的初始数据库连接数 携程技术沙龙架构专场 588 4.2 总体设计 上图简要描述了 Apollo 的总体设计,我们可以从下往上看:  ConfigService 提供配置的读取、推送等功能,服务对象是 Apollo 客户端  AdminService 提供配置的修改、发布等功能,服务对象是 Apollo Portal(管理界面)  ConfigService 和 Admin Service 都是多实例、无状态部署,所以需要将自己注册到 Eureka 中并保持心跳  在 Eureka 之上我们架了一层 Meta Server 用于封装 Eureka 的服务发现接口  Client 通过域名访问 Meta Server 获取 Config Service 服务列表(IP+Port),而后直接 通过 IP+Port 访问服务,同时在 Client 侧会做 load balance、错误重试  Portal 通过域名访问 Meta Server 获取 Admin Service 服务列表(IP+Port),而后直接 通过 IP+Port 访问服务,同时在 Portal 侧会做 load balance、错误重试  为了简化部署,我们实际上会把 Config Service、Eureka 和 Meta Server 三个逻辑角色 部署在同一个 JVM 进程中 4.2.1 Why Eureka 为什么我们采用 Eureka 作为服务注册中心,而不是使用传统的 zk、etcd 呢?我大致总结 携程技术沙龙架构专场 589 了一下,有以下几方面的原因: 它提供了完整的 Service Registry 和 Service Discovery 实现 首先是提供了完整的实现,并且也经受住了 Netflix 自己的生产环境考验,相对使用起来会 比较省心。 和 Spring Cloud 无缝集成 1)我们的项目本身就使用了 Spring Cloud 和 Spring Boot,同时 Spring Cloud 还有一套非 常完善的开源代码来整合 Eureka,所以使用起来非常方便。 2)另外,Eureka 还支持在我们应用自身的容器中启动,也就是说我们的应用启动完之 后,既充当了 Eureka 的角色,同时也是服务的提供者。这样就极大的提高了服务的可用 性。 3)这一点是我们选择 Eureka 而不是 zk、etcd 等的主要原因,为了提高配置中心的可用性 和降低部署复杂度,我们需要尽可能地减少外部依赖。 Open Source 最后一点是开源,由于代码是开源的,所以非常便于我们了解它的实现原理和排查问题。 4.3 客户端设计 上图简要描述了 Apollo 客户端的实现原理: 1、客户端和服务端保持了一个长连接,从而能第一时间获得配置更新的推送。 2、客户端还会定时从 Apollo 配置中心服务端拉取应用的最新配置。 携程技术沙龙架构专场 590 1)这是一个 fallback 机制,为了防止推送机制失效导致配置不更新 2)客户端定时拉取会上报本地版本,所以一般情况下,对于定时拉取的操作,服务端都会 返回 304 - Not Modified 3)定时频率默认为每 5 分钟拉取一次,客户端也可以通过在运行时指定 System Property: apollo.refreshInterval 来覆盖,单位为分钟。 3、客户端从 Apollo 配置中心服务端获取到应用的最新配置后,会保存在内存中 4、客户端会把从服务端获取到的配置在本地文件系统缓存一份 在遇到服务不可用,或网络不通的时候,依然能从本地恢复配置 5、应用程序可以从 Apollo 客户端获取最新的配置、订阅配置更新通知 4.3.1 配置更新推送实现 前面提到了 Apollo 客户端和服务端保持了一个长连接,从而能第一时间获得配置更新的推 送。 长连接实际上我们是通过 Http Long Polling 实现的,具体而言:  客户端发起一个 Http 请求到服务端  服务端会保持住这个连接 30 秒  如果在 30 秒内有客户端关心的配置变化,被保持住的客户端请求会立即返回,并告 知客户端有配置变化的 namespace 信息,客户端会据此拉取对应 namespace 的最新 配置  如果在 30 秒内没有客户端关心的配置变化,那么会返回 Http 状态码 304 给客户端  客户端在服务端请求返回后会自动重连 考虑到会有数万客户端向服务端发起长连,在服务端我们使用了 async servlet(Spring DeferredResult)来服务 HttpLong Polling 请求。 4.4 可用性考虑 配置中心作为基础服务,可用性要求非常高,下面的表格描述了不同场景下 Apollo 的可用 性: 携程技术沙龙架构专场 591 五、Contribute to Apollo Apollo 从开发之初就是以开源模式开发的,所以也非常欢迎有兴趣、有余力的朋友一起加 入进来。 服务端开发使用的是 Java,基于 Spring Cloud 和 SpringBoot 框架。客户端目前提供了 Java 和.Net 两种实现。 Github 地址:https://github.com/ctripcorp/apollo 欢迎大家发起 Pull Request! 携程技术沙龙架构专场 592 去哪儿全链路跟踪及 Debug [作者简介]王克礼,去哪儿平台事业部基础架构 Java 开发工程师,参与开发和维护去哪儿 内部中间件,包括配置中心、消息队列、日志收集及链路跟踪系统 QTracer 等。 现场视频:https://v.qq.com/x/page/z05351cxvnp.html 随着公司业务的发展,支持业务的程序也会逐步发展;随着业务的复杂化和流量的增加, 一般都会通过拆分的方式来分解不同的业务,将流量分摊到更多的机器上,从而支撑更复 杂的业务和更大的流量。 这种分布式的系统会带来很多好处,也自然带来了一些问题。分布式意味着需要通过网络 来进行调用,比如 RPC 调用、HTTPAPI 调用、消息队列等;同时,不只是内部开发的程序 是分布式的,程序依赖的很多服务也是分布式的,比如数据库、缓存、HBase、ES 等。大 量的分布式导致服务间的调用关系越来越来越复杂,处于分布式系统中某个节点的程序无 法方便的掌握全局结构。 为了方便掌握分布式系统的全局情况,出现了一种分布式追踪系统,它能够将请求所经过 的各个系统的操作用一个唯一 ID 标识并记录下来,便于查看和分析系统全局结构。 QTracer 就是 Qunar 内部开发维护的一套分布式追踪系统。 一、QTracer 简介 1、简介 QTracer 是 Qunar 内部开发维护的一套分布式追踪系统;它会为每个请求生成一个全局唯 一的 TraceID,然后将 TraceID 不断传递给下游系统;同时,在每个系统中,它都会记录各 个系统里的各项操作;最后,通过 TraceID 将各个系统里记录的操作整合起来,还原出一 个请求在整个分布式系统中的详细执行流程。 2、功能介绍 下面简单介绍一些 QTracer 提供的功能,让读者能够快速了解 QTracer 的功能,直观的感 受 QTracer 的作用。 2.1 执行链路查询 携程技术沙龙架构专场 593 链路查询是 QTracer 的基础功能之一,它能够将整个调用流程完整的展示出来。上图就是 链路展示页面,从图中能看到请求所在机房、描述、类型、执行时间等信息。 链路查询能够起到很多作用:  它能清晰展示整个请求链路,帮助使用者快速了解全局情况。  能够了解请求经过了哪些服务、哪些机器、耗时情况、跨机房调用情况等。  能够了解各个服务的执行情况,比如是否执行成功、是否进行了重试、失败是否对整 个请求造成了影响。  能够快速看出整个请求的耗时分布,快速了解请求的瓶颈。 2.2 关联日志查询 除了查询执行链路,QTracer 还提供了根据 TraceID 查询关联日志的功能。上图就是关联日 志查询的效果,能够展示出请求经过的各个系统上对应的日志。当你不能从链路数据里看 到足够的信息时,可以查看相关的日志,往往日志中记录了更详细的信息。 2.3 按条件搜索 携程技术沙龙架构专场 594 前面说明的两个功能都是需要根据 TraceID 来查询,有时我们需要的却是查询 TraceID 的功 能。上图就展示了 QTracer 提供的 TraceID 搜索功能,可以根据 TraceID 前缀、起始应用、 时间段、关键字等来搜索出对应的 TraceID。 搜索时的关键字就是业务在操作执行时记录到 Trace 数据里的;比如可以将订单号记录到 Trace 数据中,这样后面就可以根据订单号查询出对应的 TraceID,从而还原订单的处理流 程。 2.4 服务上下游关系 上面提到的功能都是直接查询、搜索原始的 Trace 数据,实际上 QTracer 通过对 Trace 链 路的实时分析,提供了更多的功能。上图中的服务上下游关系就是一例,通过对 Trace 链 路中的调用关系进行分析,可以得到服务间的依赖关系,也能得到服务调用的 QPS、耗时 等数据。 2.5 数据库操作统计 携程技术沙龙架构专场 595 除了对服务进行分析,QTracer 还对数据库操作等进行了分析。上面的图就展示了表级、 语句级的执行次数统计。实际上 QTracer 提供的数据库操作相关统计更多,一是提供了 库、表、语句三个维度的 QPS、耗时情况统计,方便了解执行情况;二是提供了最近最慢 数据操作的统计,方面提前发现问题,比如某些索引没有正确建立。 2.6 透明数据传递 Trace 链路记录的时候就要贯穿多个系统,它是否能够作为一个旁路来传递数据呢? QTracer 据此提供了透明数据传递功能,利用 Trace 链路不断传递的特性将上游数据向下游 不断传递,避免各个业务为了非业务参数而修改接口。 透明数据传递主要用来传递一些开关、标识,或者是一些非请求业务相关的数据。下面说 几个例子: 携程技术沙龙架构专场 596 1.ABTest 时可以使用它传输分支标识,从而控制流程走向。 2.单元化服务里利用它传递单元标识符,避免跨单元调用。 2.7 其它 当然,前面介绍的不是 QTracer 的所有功能,只是举了一些典型功能。除此之外,QTracer 还提供了异常自动关联 TraceID、服务调用 QPS 分析、最近最慢服务调用统计、操作失败 情况统计等功能。 二、QTracer 客户端核心设计 1、数据模型 上图展示了 QTracer 中一个 Trace 的基本结构,Trace 由多个 Span 组成。 Trace 是由多个 Span 聚合而成的树形结构,它表示一次完整的请求链路。 Span 是 Trace 的基本组成单元,它表示请求涉及到的一个个单独记录的操作。Span 中保 存了服务描述、操作起始结束时间、操作结果、操作类型等信息。 2、基本概念 在 Span 中记录了许多不同作用的数据,它们都有不同的作用场景,下面笔者将逐个介绍 Span 中涉及到的这些基本概念。 2.1 TraceID TraceID 简单来说就是一个全局唯一 ID,QTracer 利用它来关联整个请求链路上的所有操 作。可以看出,TraceID 是一个需要在系统间不断传递的数据。 TraceID 原则上来说只需要保证全局唯一即可,使用 UUID 这种也没有关系。不过 QTracer 携程技术沙龙架构专场 597 里的 TraceID 做了一些设计,在 TraceID 里包含了更多的信息,比如起始应用、起始机器、 生成时间、采样标识等,一方面 TraceID 更加规律,另一方面也方便调查 Trace 相关的问 题。 2.2 SpanID TraceID 标识了整个调用链路,而 SpanID 则是标记了链路中的一个个操作。通过 SpanID 可以看出服务的执行顺序和调用关系。由于需要通过 SpanID 确定关系,所以 SpanID 也需 要不断传递。 下面简单介绍下 QTracer 的 SpanID 设计方案: 1.取 SpanID 为 1 的 Span 作为 Root Span,表示一个请求的起点。 2.标记同一级的顺序调用时,SpanID 的本级不断增长,比如 1.1、1.2、1.3 这种。 3.标记调用关系时,则需要增加 SpanID 的层级,比如 1、1.1、1.1.1 这样。 2.3 TimelineAnnotation TimelineAnnotation 用于记录一个 Span 内部的时序性信息。例如: 1.HTTP 客户端的一次请求记为一个 Span 2.实际上一次请求会有多个步骤,比如建立连接、写入请求、读取回复等 3.TimelineAnnotation 就是记录这种内部的时序性操作的 通过 TimelineAnnotation 可以看到各个阶段的具体起始时间,一个简单的应用场景就是当 HTTP 请求失败时,能通过它看到进行到哪个步骤失败了,方便快速定位问题。 2.4 KVAnnotation KVAnnotation 是最常用的一种,它用于记录业务自己关心的自定义数据,比如订单号、 UID 等。记录之后,查询 Trace 链路是能看到这些记录的数据,也能通过这些数据反向搜 索出相关的 TraceID。 2.5 TraceContext TraceContext 就是透明数据传递需要使用到的概念,它也是保存业务自定义数据的,但是 TraceContext 中的数据不会被收集,而是不断的随着链路向下游传递。 3、核心 API 携程技术沙龙架构专场 598 上图展示了 QTracer 的核心 API 使用方式,主要就是利用 startTrace 函数开启一个新的 Span,然后利用各个 add 函数添加不同种类的数据。 实际上,用户基本不需要使用这些核心 API,公司里的很多组件都默认添加了 QTracer 的 埋点,直接使用默认埋点在配合一些快捷使用方式基本就足够了。 4、Trace 的延续 为了关联多个系统的操作,必须要把上下文不断的传递下次,所以 Trace 的延续就是一个 关键性的问题。 首先,我们先介绍一下单个系统内部如何延续 Trace 链路,它分为同步调用和异步及跨线 程调用两种情况。 1.同步调用。同步调用时,延续 Trace 链路非常简单,QTracer 内部会利用 ThreadLocal 来 保存上下文关系,每次开启新的 Span 时,直接从 ThreadLocal 里获取当前的 TraceID 和 SpanId 即可。 2.异步调用及跨线程调用。这种情况下,ThreadLocal 是无法生效的,只能显式的延续上下 文关系。对于线程及线程池,QTracer 提供了快速延续的包装方法,使用也非常方便;而 异步调用则只得利用核心 API 进行延续。 其次,除了单个系统内部的传递,还有许多情况需要跨机器延续。分布式情况下跨网络请 求时非常常见的,跨网络的情况和跨线程是非常相似的,都是需要手工进行延续。为了方 便地跨网络传输上下文关系,QTracer 在内部使用的 Dubbo、HTTP、MQ 等组件里都加入 了自动传输上下文的功能,一般来说不需要使用方关注。 5、无侵入埋点 前面提到很少需要使用核心 API,因为直接使用核心 API 记录对用户不够友好,太繁琐 了。为了方便使用者,我们对各个组件都添加了无侵入的埋点,用户直接开启采样就能得 到足够的信息,基本不需要自己记录过多的数据。 携程技术沙龙架构专场 599 无侵入埋点主要包含两种情况: 1.对于公司内部维护的组件,可以直接添加埋点代码,这样能够更精确的控制功能,记录 更多信息。比如 Dubbo、消息队列这种组件。 2.对于不是公司内部维护的组件,由于无法修改源码添加埋点功能,所以采用了字节码修 改的方式,能够在运行时为指定的类添加埋点功能。比如 MySQL 和 PG 的 driver。 6、字节码插桩 字节码插桩功能在 QTracer 中使用非常广泛,这里简单介绍一下。字节码插桩就是一种运 行时动态修改字节码的技术,能动态调整代码的行为。它和代理的作用很像,但是实现上 完全不同。一般来说需要为 JVM 添加代理(agent)来启用字节码插桩功能。 QTracer 实现了一套利用配置指定对某些类的方法进行插桩的功能。比如,针对 MySQL 和 PG 的操作的插桩就是通过在配置文件中指定驱动中的方法、字段等实现的。同时,当有有 新的客户端需要插桩接入时,直接在配置中心添加新的插桩配置即可直接生效。 7、本地方法快速插桩 除了中间件、数据库 driver 等预先埋点的组件,有些业务系统还想要跟踪一些重要的本地 方法。这种时候直接使用核心 API ?核心 API 对业务来说使用起来比较麻烦,需要熟悉和 API 的使用,避免使用错误,同时也会加入很大业务无关代码。 为了解决这个问题,QTracer 提供了一个方便的解决方案:注解。利用注解标记需要跟踪 的方法和需要记录的数据。然后程序编译时自动生成一份本地的插桩配置,启动时 QTracer 载入这个本地配置即可自动对那些指定的方法进行插桩。 上图展示了几个注解的使用场景,@QTrace 注解标记要跟踪的方法,@QP 标记要记录的 参数,@QF 标记要记录的成员变量。 8、日志关联 QTracer 提供了根据 TraceID 查询关联日志的功能,但是日志是如何关联到 TraceID 的呢? QTracer 在实现时利用了 MDC(Mapped Diagnostic Context)来保存 TraceID 和 SpanID, 携程技术沙龙架构专场 600 MDC 中的数据是可以直接输出到日志中的。 Span 生成时将 TraceID 和 SpanID 保存到 MDC 中,等一个 Span 结束时将这两种数据清 空;这样一来,在 Span 表示的操作期间,所有记录的日志都能够同时记录当前的 TraceID 和 SpanID。使用方通过配置日志的 log pattern,将 QTracer 的信息默认输出到日志中。后 面等日志被实时日志收集系统收集上来的时候,利用实时任务分析日志即可获得 TraceID 到日志的关联关系。 三、QTracer 系统架构 这是整个 QTracer 系统的简单架构图,包括数据记录与收集、数据处理分析、数据展现这 三个部分。 1、Trace 数据记录和收集 QTracer 利用本地日志进行数据记录,将数据全部暂存到本地日志,然后利用实时日志收 集将数据全部发送到专门的 Kafka 集群暂存。 数据记录时尤其重要的就是控制资源消耗,插桩时要尽量降低额外损耗,记录日志时也要 小心对 IO 和磁盘空间的占用情况。为了尽量降低记录日志的损耗,QTracer 内部实现了异 步批量写日志;控制批量大小,避免占用过多内存;日志文件按照大小轮转而不是时间轮 转,同时严格控制日志文件数量,这样能避免大量数据占据过多磁盘空间;同时在极端情 况下,为了避免过多占用资源,QTracer 会选择丢弃一定量的数据。 2、Trace 数据处理与分析 我们内部选择了 Samza 框架作为分析 Trace 数据的实时任务框架,主要是因为: 1. Samza 和 Kafka 集成程度高,配合良好 2. Samza 使用非常简单,API 简单直接,功能在 Trace 数据分析这个场景上足够使用 携程技术沙龙架构专场 601 3.提供了一个基于 rocksdb 的本地 cache,提供了异常恢复功能,方便编写需要缓存很多数 据做聚合的任务 上图展示了 Trace 数据处理的一个大致流程。数据处理主要分为两条线路,针对 Span 进行 处理和针对 Trace 链路进行处理。 针对 Span 进行的处理主要有: 1.直接保存到 HBase 中提供快速查询。Span 保存到 HBase 时,直接使用 TraceID 构成 rowkey,使用 SpanID 作为列名,这样能提供非常快速的查询。 2.利用实时任务将 Span 数据聚合形成完整的 Trace 链路。 3.分析最近最慢操作,比如 HTTP 请求、数据库操作等,这种数据能够根据单独的 Span 数 据分析出来。 4.统计操作的 QPS、耗时等数据。 针对 Trace 进行的处理主要有: 1.对完成的 Trace 链路数据进行精简,删除重复数据和无用数据,然后保存到 ES 中提供多 维度搜索功能。适度的精简能降低数据量,不保存冗余的数据。 2.分析统计调用链路上的上下游调用关系。通过对完整的链路进行拆解,能够得到链路涉 及的各个服务的上下游关系。 3.分析上下游调用关系的同时,也能得到服务调用的 QPS、耗时情况等。 携程技术沙龙架构专场 602 4.通过大量调用关系的聚合,能够得到整体上各个服务之间的依赖关系。有依赖关系之 后,查问题时更容易定位问题。 3、数据存储 QTracer 存储方面主要使用了 HBase 和 ES 两种。它们各自都有不同的作用,下面简单介绍 一下。 HBase 首先是用于保存 Trace 链路数据,这点前面也提到过;实际保存时为了避免 HBase region 写入过热,对 TraceID 做了一些变形,将分布均匀的字段尽量放到 rowkey 前面。其 次,HBase 还保存了根据服务调用统计出来的分钟级 QPS、调用时间等数据。 ES 首先是用于保存精简的 Trace 链路,提供了按条件搜索功能。其次,还保存了服务的上 下游调用关系、近期的整个服务依赖关系等。 四、QTracer Debug 1、简介 除了 QTracer 和各个组件提供的预先埋点以及手工提前埋点,有时会遇到想要直接获取代 码运行到某个位置是调用栈的具体状态。QTracer Debug 就提供了这样一种功能,它类似 于 IDE 中提供的 debug 功能,通过在源码中设置断点,可以获取实际代码运行时断点处的 所有变量、调用栈信息,而且,这不会暂停应用,同时额外损耗也非常小,可以直接在线 上使用。 下面简单说下使用流程: 1.在前端页面上选择项目,浏览代码,在代码行上标记断点 2.选择应用的机器,启用断点 3.访问能经过断点的 URL,带上指定的参数 4.等待数据收集完成即可自动展示所有数据 2、详细实现 2.1 概要 携程技术沙龙架构专场 603 展示层面借助 Gitlab 的 API 实现代码浏览和按行设置断点功能。然后借助 QTracer 的字节 码插桩功能在指定位置加入断点代码,执行时进入断点就能记录调用栈状态。直接利用 QTracer 现有的数据处理路径进行数据的记录和收集。后面的实时任务会筛选出含有 debug 数据的 Trace 链路,并提取断点数据保存到 HBase。最后在前端展示断点数据。 2.2 断点添加 断点添加是一个核心部分,它的主要流程是: 1.分析应用所有的类,建立源文件+行号到类的映射关系。 2.根据断点位置找到需要添加断点的类 3.分析类,收集类中所有变量的作用域信息 4.修改类的字节码,在指定位置插入收集所有作用域内变量等数据的代码 2.3 数据收集及记录 QTracer Debug 的数据记录直接依赖 QTracer 自身的 KVAnnotation 功能,直接把数据存放 到 QTracer 的 Span 中。数据收集直接通过 QTracer 的写本地日志+日志收集方式。实时任 务对 QTracer 数据进行筛选,保存到 HBase 中。 携程技术沙龙架构专场 604 高吞吐消息网关的探索与思考 [作者简介]刘惊惊,唯品会业务架构部高级架构师。主要负责用户线,营销线的业务架 构,也参与库存系统的重构改造。本文来自刘惊惊在“携程技术沙龙——海量互联网基础 架构”上的分享。 现场视频:https://v.qq.com/x/page/u0539ixs9zs.html 一、背景介绍 唯品会是一家立足于“全球精选,正品特卖”的电商网站,拥有 4 亿注册会员,日活约 2 千万会员。随着会员数量的增多,公司业务部门的飞速发展,和用户的沟通变得日益重 要。沿用至今的消息网关,面对多变的业务和爆发式增长的消息面前,显得力不从心,多 次大促出现性能瓶颈,急需重构来跟上公司业务发展的需要。 二、唯品会消息网关的架构定位 在本次重构中,将原来耦合在一起的消息发送渠道,被拆分成逻辑消息网关和物理发送渠 道。逻辑消息网关(后面均用消息网关来代替)作为消息发送的总入口,对接上游各个业 务系统,为业务系统提供友好的发送受理服务。逻辑消息网关的下游,对接各个物理投递 渠道,负责对接第三方信道提供方(如电信,微信等)。 在重构前,消息网关和营销系统耦合较紧,包括定制化功能和共用的开发团队。在重构 后,逐渐剥离了这种关系,使得消息网关定位为公司级的基础服务层。其主要职责是:受 理投递、频次控制、尽力发送、反馈统计、削峰匹配慢速信道。 下面介绍一下消息网关在唯品会整体系统中的定位。如图 1 所示,消息网关定位于基础业 务服务层,而物理信道定位在基础设施层,两者之间明确的区分了是否业务相关。 携程技术沙龙架构专场 605 图 1 消息网关在唯品会整体架构中的位置 那么消息网关具体内部应该是个什么样子,下面我们一起来看一下图 2 消息网关的内部构 造。一个合格的消息网关应该具有以下的功能:业务友好的受理层,模板管理,频次控 制,基于优先级队列的消息分发,反馈统计,延时发送,订阅控制,以及其他一些辅助功 能。在第二部分将逐个模块的讲解。 图 2 消息网关内部构造 三、如何设计消息网关 在图 2 中,我们全面概览了消息网关内部应该具备的各个功能模块,下面我们逐个模块分 解,看看各个部分的功能模块应该如何设计。 1、消息的受理和分发 携程技术沙龙架构专场 606 在实际的业务场景中,发送给会员,供应商和内部工作人员的消息,分为三种类型:关键 性消息,通知类消息,以及营销类消息。关键性消息的特点是,低时延,低吞吐。通知类 消息的特点是中时延,高吞吐。营销类消息的特点是中时延,高吞吐。针对不同的消息类 型,应该选择不同的受理和分发模块,避免互相干扰。如图 3 所示。 图 3 消息的受理和分发 对于关键性消息,某些渠道如短信,必须设置专用的独占信道来满足业务要求。如图 4 所 示,中国移动信道质量较其他的供应商更好,所以对于关键性消息,通过专线连接主要服 务商,并同时接入电信联通 haproxy 做备用线路。 图 4 短信消息网关分发架构 2、面向业务友好的接口 对使用消息发送服务的业务方来说,最好的接入方式是访问配置好模板的统一接口(对于 携程技术沙龙架构专场 607 需要高度个性化的营销系统,是个例外,营销系统倾向于在系统内部完成消息个性化的逻 辑),尽量保持代码接口调用的固定,对于少量变动通过修改配置的方式完成。 这么做有两点好处,一是消息网关内部的优化和渠道变动逻辑,不需要被业务系统感知。 二是使用预设模板降低了系统交互的开销。图 5 展示了统一接入接口在系统中的位置。 图 5 面向业务友好的接口 3、频次控制 消息网关需要保护会员不被过多的打扰,提供重要的兜底功能。随着业务的发展,商城、 金融系统逐步独立,分散的营销系统往往无法从全局上兼顾整体营销。在营销强度模块正 式运行之前,控制过度营销的最后一道闸门,控制在消息网关这里。 图 6 展示了消息网关在统一接入接口处检查频次控制的情况,对于超过限额的发送请求, 会直接拒绝受理。 图 6 频次控制 携程技术沙龙架构专场 608 4、用户订阅 现代消息发送系统都会提供用户退订功能,允许用户对希望接收的渠道和功能进行自主选 择。那么用户订阅的关系应该维护在哪个系统比较合适呢?在实践中发现订阅关系维护在 会员系统比较合适,消息网关查询订阅关系通过接口访问加缓存的方式去获取。同时各个 渠道的退订消息,也需要经由消息网关上报到会员系统,更新订阅关系。图 7 展示了用户 订阅的调用图。 图 7 用户订阅 5、失败重试 消息网关调用下游物理网关,不可避免的会出现调用失败的情况。那么这个时候就需要进 行失败重试。失败重试分为 2 大类,重试失败需要落盘的,和重试失败直接记录异常日志 的。验证码属于后者。对于通知类消息,可以容忍一定的时延,采用落盘定时任务轮询重 试的方式比较合适。对于营销类消息,在时效期内可以落盘重试,极端情况也可以采用记 录异常日志,然后直接丢弃的方式。 携程技术沙龙架构专场 609 图 8 失败重试 6、反馈统计 从业务系统投递消息开始,会经过消息网关,物理消息渠道,第三方供应商等多个环节。 每个环节都有可能造成消息的丢失,因此设计反馈统计机制很有必要,在错误排查和费用 统计等方面有重要作用。实践中,采用反馈队列的方式进行异步统计,有分钟级的延时。 对消息的最终发送结果,ETL 到大数据,进行费用统计和费用分摊计算。 图 9 反馈统计 四、消息网关的技术选型 业务模块开发采用唯品会自研的 RPC 调用框架 Venus,采用 OSP 协议(唯品会私有协议) 来封装 RPC 调用,底层通信协议采用 Netty,传输协议使用 Thrift。数据库技术选型 MySQL。消息队列用 Kafka,应对高吞吐量消息场景。ETL 使用自研 VDP(类似于阿里开源 的 Canal),解析 binlog,同步数据到 HDFS。图 10 演示了 Venus 框架的基本结构。 携程技术沙龙架构专场 610 Venus 框架是一个去中心化的 RPC 调用框架,Client 端对 Server 端的调用由 proxy 进行转 发。Client 到 LocalProxy,以及 Local Proxy 到 Server 端保持 TCP 长连接。 Proxy 提供跨语言支持服务治理,服务发现,路由调度,限流熔断等功能,并额外支持 HTTP GET +URL Path/Parameter,HTTP POST +JSON 和 RPC OSP 协议的转换。Proxy 分为 LocalProxy 和 Remote Proxy。Local Proxy 和 Client 部署在同一台服务器上,通过 Domain Socket,减少 TCP 栈的调用开销。Remote Proxy 作为备用链路,提供防火墙穿透,跨组织 调用,非 Java 语言 HTTP +JSON 调用等功能。 图 10 Venus 框架基本结构 消息网关重度依赖 kafka 队列,分成消息受理队列,消息异步落盘队列,延时写入队列, 反馈队列等。由于整个消息网关基本都是异步化操作,消息的分发有可能早于消息的落 盘,这样在数据库消息发送状态更改时,就会出现无法找到的情况。可以采用延时队列, 对消息发送状态的落盘动作进行延时写入。 五、消息网关的监控和降级 1、消息网关的监控 在日常运维层面,消息网关需要监控如下的指标:一是受理域,分发域各接口的调用情 况。这部分数据已经在 Venus 框架内置,导入 Mercury 监控系统(类似于携程开源的 CAT 系统)。二是 Kafka 队列的消息堆积情况监控。三是采用 Zabbix 监控数据库服务器的 CPU 负载,内存使用量,磁盘 IO 等关键性指标。四是消息网关内部监控各物理渠道的投递时 延。 携程技术沙龙架构专场 611 图 11 受理域请求监控 图 12 物理渠道的投递时延监控 携程技术沙龙架构专场 612 图 13 Kafka 消息堆积监控 2、消息网关的降级措施 应用于生产系统的消息网关需要部署降级预案,保证故障发生时可以尽快的恢复,降低故 障带来的负面影响。下面列出了部分重要的降级预案。  受理域机器宕机,由 Local Proxy 调度转发到正常工作的机器上。  Kafka 无法提供服务,运维提供分钟级紧急恢复,期间消息发送受理中断。已入队列 但尚未发送消息,在原集群恢复后继续发送。  数据库主库宕机,影响消息状态变更和消息异步落盘。切换到从库。不影响消息的正 常发送,影响反馈和统计。  用户系统订阅关系无法返回结果,降级为不再查询,默认全部订阅。  物理消息网关宕机,投递失败,有重试机制保证。消息本身不丢失,可以在物理渠道 恢复后,重发有效期内的消息。 六、消息网关的几点思考 1、业务边界的划分 消息网关本身的设计并不复杂,使用的技术也是成熟的技术。消息网关的难点在于,对于 系统业务边界的划分。由于和消息网关打交道的业务系统较多,很多功能可以由上游完 成,也可以由下游实现,这个时候就需要权衡各个方面,找到折中的方案。在实践中,唯 品会是这么划分业务系统边界的。  用户系统管理用户订阅情况,消息网关管理订阅大类和消息模板的映射。  消息网关管理消息逻辑层分发,物理发送渠道管理具体投递行为。  消息网关管理渠道沟通频次,营销系统管理营销沟通频次。  风控系统提供消息防刷机制,易被攻击服务经过风控验证码服务接入消息网关。  消息网关提供简单消息模板管理功能,非营销类业务系统不自行管理模板。  人群标签由用户画像提供,渠道动态规划由消息网关承载。  营销系统个性化消息组装,由营销系统自行负责,消息网关只提供发送功能。 携程技术沙龙架构专场 613 图 14 消息网关业务边界的划分 2、消息网关数据的使用 消息网关的投递的营销消息,订阅情况,打开率等是营销画像的重要组成部分。全景营销 平台可以将消息投递情况、打开率、花费作为营销强度计算的数据源。 消息网关数据是各个业务部门费用分摊的主要依据,涉及上市公司的财务审计。 另外通过消息网关数据统计,可以作为各个渠道服务提供商选择和替换的依据。 3、一些踩过的坑 短信物理网关的通讯协议按照运营商的不同,分成 CMPP/SMGP/SGIP 三种。都是基于 TCP 协议上的私有协议,需要自行处理断包和粘包问题。需要将包解析类 CmppDecoder(移 动)/SmgpDecoder(电信)/SgipDecoder(联通)配置到 Mina 框架 NioSocketConnector 的 FilterChain 里面。用 IoSession 来完成收发操作。 消息网关异步化引起的问题,比如消息数据异步落盘,可能落后于消息发送状态的更新。 需要引进延时队列,通过定时重试解决此问题。 Redis 使用上的最佳实践,比如 Redis 存储 Value 的 String 不要超过 2k 字节(理论最大值 512M),以不超过 1 个网络包(MTU 1500byte)为佳。Set 和 Sorted 的 elements 不要超过 5000 个。 压力测试机器上配置的最佳实践,比如需要配置/etc/sysctl.conf 的选项: net.ipv4.tcp_syncookies= 1 net.ipv4.tcp_tw_reuse =1 net.ipv4.tcp_tw_recycle= 1 携程技术沙龙架构专场 614 net.ipv4.tcp_fin_timeout= 15 七、小结 这篇文章总结了唯品会在消息网关重构中,使用的架构思路和经验。文中提到的消息网 关,在今年 6.16 大促高峰期承担了亿级的消息发送压力,经过实践的检验,稳定可靠。希 望对于行业内有相同应用场景的公司,能提供一定的设计借鉴作用。系统的设计,无法完 美,希望不吝赐教,共同构建起更加高效和稳定的系统。 携程技术沙龙 AI 专场 615 携程技术沙龙 AI 专场 携程技术沙龙 AI 专场 616 阿里小蜜-电商领域的智能助理技术实践 [作者简介]陈海青,阿里巴巴智能服务事业部资深技术专家,在阿里从事智能人机交互领 域相关的工作和研究 8 年,带领团队构建了阿里巴巴智能交互机器人系统。 现场视频:https://v.qq.com/x/page/w05629wz0e4.html 一、智能人机交互领域的介绍 1.1 行业分类及目前的应用状况 在全球人工智能领域不断发展的今天,包括 Google、Facebook、Microsoft、Amazon、 Apple 等互联公司相继推出了自己的智能私人助理和机器人平台。 智能人机交互通过拟人化的交互体验逐步在智能客服、任务助理、智能家居、智能硬件、 互动聊天等领域发挥巨大的作用和价值。因此,各大公司都将智能聊天机器人作为未来的 入口级别的应用在对待。今天随着市场的进一步发展,聊天机器人按照产品和服务的类型 主要可分为:客服,娱乐,助理,教育,服务等类型。 图 1 截取了部分聊天机器人。 图 1:一些 chat-bot 的汇总 1.2 阿里小蜜在电商领域的状况 2015 年 7 月,阿里推出了自己的智能私人助理-阿里小蜜,一个围绕着电子商务领域中的 服务、导购以及任务助理为核心的智能人机交互产品。通过电子商务领域与智能人机交互 领域的结合,带来传统服务行业模式的变化与体验的提升。 携程技术沙龙 AI 专场 617 在去年的双十一期间,阿里小蜜整体智能服务量达到 643 万,其中智能解决率达到 95%, 智能服务在整个服务量(总服务量=智能服务量+在线人工服务量+电话服务量)占比也达到 95%,成为了双十一期间服务的绝对主力。 二、电商领域下阿里小蜜的技术实践 2.1 阿里小蜜技术的 overview 智能人机交互系统,俗称:chatbot 系统或者 bot 系统。图 2 是人机交互的流程图: 图 2:人机交互的流程 核心是 NLU(自然语言理解),通过对话系统处理后,最后通过自然语言生成的方式给出答 案。一段语言如何理解对于计算机来说是非常有难度的,例如:“苹果”这个词就具备至 少两个含义,一个是水果属性的“苹果”,还有一个是知名互联网公司属性的“苹果”。 2.1.1 意图与匹配分层的技术架构体系 在阿里小蜜这样在电子商务领域的场景中,对接的有客服、助理、聊天几大类的机器人。 这些机器人,由于本身的目标不同,就导致不能用同一套技术框架来解决。因此,我们先 采用分领域分层分场景的方式进行架构抽象,然后再根据不同的分层和分场景采用不同的 机器学习方法进行技术设计。首先我们将对话系统从分成两层: 1、意图识别层:识别语言的真实意图,将意图进行分类并进行意图属性抽取。意图决定了 后续的领域识别流程,因此意图层是一个结合上下文数据模型与领域数据模型不断对意图 进行明确和推理的过程; 2、问答匹配层:对问题进行匹配识别及生成答案的过程。在阿里小蜜的对话体系中我们按 照业务场景进行了 3 种典型问题类型的划分,并且依据 3 种类型会采用不同的匹配流程和 方法: 携程技术沙龙 AI 专场 618 问答型:例如“密码忘记怎么办?”→ 采用基于知识图谱构建+检索模型匹配方式 任务型:例如“我想订一张明天从杭州到北京的机票”→ 意图决策+slots filling 的匹配以 及基于深度强化学习的方式 语聊型:例如“我心情不好”→ 检索模型与 Deep Learning 相结合的方式 图 3 表示了阿里小蜜的意图和匹配分层的技术架构。 图 3:基于意图于匹配分层的技术架构 2.1.2 意图识别介绍:结合用户行为 deep-learning 模型的实践 通常将意图识别抽象成机器学习中的分类问题,在阿里小蜜的技术方案中除了传统的文本 特征之外,考虑到本身在对话领域中存在语义意图不完整的情况,我们也加入了用实时、 离线用户本身的行为及用户本身相关的特征,通过深度学习方案构建模型,对用户意图进 行预测, 具体如图 4: 携程技术沙龙 AI 专场 619 图 4:结合用户行为的深度学习意图分类 在基于深度学习的分类预测模型上,我们有两种具体的选型方案:一种是多分类模型,一 种是二分类模型。多分类模型的优点是性能快,但是对于需要扩展分类领域是整个模型需 要重新训练;而二分类模型的优点就是扩展领域场景时原来的模型都可以复用,可以平台 进行扩展,缺点也很明显需要不断的进行二分,整体的性能上不如多分类好,因此在具体 的场景和数据量上可以做不同的选型。 小蜜用 DL 做意图分类的整体技术思路是将行为因子与文本特征分别进行 Embedding 处 理,通过向量叠加之后再进行多分类或者二分类处理。这里的文本特征维度可以选择通过 传统的 bag of words 的方法,也可使用 Deep Learning 的方法进行向量化。具体图: 图 5:结合用户行为的深度学习意图分类的网络结构 2.1.3 匹配模型的 overview:介绍行业 3 大匹配模型 目前主流的智能匹配技术分为如下 3 种方法: 1、基于模板匹配(Rule-Based) 2、基于检索模型(Retrieval Model) 3、基于深度学习模型(Deep Learning) 在阿里小蜜的技术场景下,我们采用了基于模板匹配,检索模型以及深度学习模型为基础 的方法原型来进行分场景(问答型、任务型、语聊型)的会话系统构建。 2.2 阿里小蜜的 3 大领域场景的技术实践 2.2.1 智能导购:基于增强学习的智能导购 智能导购主要通过支持和用户的多轮交互,不断的理解和明确用户的意图。并在此基础上 利用深度强化学习不断的优化导购的交互过程。图 6 展示了智能导购的技术架构图。 携程技术沙龙 AI 专场 620 图 6:智能导购的架构图 这里两个核心的问题: a)在多轮交互中理解用户的意图。 b)根据用户的意图结果,优化排序的结果和交互的过程。 下面主要介绍导购意图理解、以及深度增强学习的交互策略优化。 2.2.1.1 智能导购的意图理解和意图管理 智能导购下的意图理解主要是识别用户想要购买的商品以及商品对应的属性,相对于传统 的意图理解,也带来了几个新的挑战。 第一,用户偏向于短句的表达。因此,识别用户的意图,要结合用户的多轮会话和意图的 边界。 第二,在多轮交互中用户会不断的添加或修改意图的子意图,需要维护一份当前识别的意 图集合。 第三,商品意图之间存在着互斥,相似,上下位等关系。不同的关系对应的意图管理也不 同。 第四,属性意图存在着归类和互斥的问题。 针对短语表达,我们通过品类管理和属性管理维护了一个意图堆,从而较好的解决了短语 携程技术沙龙 AI 专场 621 表示,意图边界和具体的意图切换和修改逻辑。同时,针对较大的商品库问题,我们采用 知识图谱结合语义索引的方式,使得商品的识别变得非常高效。下面我们分别介绍下品类 管理和属性管理。 基于知识图谱和语义索引的品类管理 智能导购场景下的品类管理分为品类识别,以及品类的关系计算。下图是品类关系的架构 图。 图 7:品类管理架构图 品类识别: 采用了基于知识图谱的识别方案和基于语义索引及 dssm 的判别模型。 a) 基于商品知识图谱的识别方案: 基于知识图谱复杂的结构化能力,做商品的类目识别。是我们商品识别的基础。 b) 基于语义索引及 dssm 商品识别模型的方案: 知识图谱的识别方案的优势是在于准确率高,但是不能覆盖所有的 case。因此,我们提出 了一种基于语义索引和 dssm 结合的商品识别方案兜底。 携程技术沙龙 AI 专场 622 图 8:基于语义索引和 dssm 的商品识别方案 语义索引的构造: 通常语义索引的构造有基于本体的方式,基于 LSI 的方式。我们用了一种结合搜索点击数 据和词向量的方式构造的语义索引。主要包括下面几步: 第一步:利用搜索点击行为,提取分词到类目的候选。 第二步:基于词向量,计算分词和候选类目的相似性,对索引重排序。 基于 dssm 的商品识别: dssm 是微软提出的一种用于 query 和 doc 匹配的有监督的深度语义匹配网络,能够较好的 解决词汇鸿沟的问题,捕捉句子的内在语义。本文以 dssm 作为基础,构建了 query 和候 选的类目的相似度计算模型。取得了较好的效果,模型的 acc 在测试集上有 92%左右。 图 9:dssm 模型的网络结构图 样本的构造:训练的正样本是通过搜索日志中的搜索 query 和点击类目构造的。负样本则 是通过利用 query 和点击的类目作为种子,检索出来一些相似的类目,将不在正样本中的 类目作为负样本。正负样本的比例 1:1。 携程技术沙龙 AI 专场 623 品类关系计算: 品类关系的计算主要用于智能导购的意图管理中,这里主要考虑的几种关系是:上下位关 系和相似关系。举个例子,用户的第一个意图是要买衣服,当后面的意图说要买水杯的时 候,之前衣服所带有的属性就不应该被继承给水杯。相反,如果这个时候用户说的是要裤 子,由于裤子是衣服的下位词,则之前在衣服上的属性就应该被继承下来。 上下位关系的计算 2 种方案: a)采用基于知识图谱的关系运算。 b)通过用户的搜索 query 的提取。 相似性计算的两种方案: a)基于相同的上位词。比方说小米,华为的上位词都是手机,则他们相似。 b)基于 fast-text 的品类词的 embedding 的语义相似度。 基于知识图谱和相似度计算的属性管理 下图是属性管理的架构图: 图 10:属性管理架构图 整体上属性管理包括属性识别和属性关系计算两个核心模块,思路和品类管理较为相似。 这里就不在详细介绍了。 2.2.1.2 深度强化学习的探索及尝试 强化学习是 agent 从环境到行为的映射学习,目标是奖励信号(强化信号)函数值最大,由环 境提供强化信号评价产生动作的好坏。agent 通过不断的探索外部的环境,来得到一个最 优的决策策略,适合于序列决策问题。图 11 是一个强化学习的 model 和环境交互的展 携程技术沙龙 AI 专场 624 示。 图 11:env-model 的交互图 深度强化学习是结合了深度学习的强化学习,主要利用深度学习强大的非线性表达能力, 来表示 agent 面对的 state 和 state 上决策逻辑。 目前我们用 DRL 主要来优化我们的交互策略。因此,我们的设定是,用户是强化学习中的 env,而机器是 model。action 是本轮是否出主动反问的交互,还是直接出搜索结果。 状态(state)的设计: 这里状态的设计主要考虑,用户的多轮意图、用户的人群划分、以及每一轮交互的产品的 信息作为当前的机器感知到的状态。 state= ( intent1, query1, price1, is_click,query_item_sim, …, power, user_inter, age) 其中 intent1 表明的是用户当前的意图,query1 表示的用户的原始 query。price1 表示当前 展现给用户的商品的均价,is_click 表示本轮交互是否发生点击,query_item_sim 表示 query 和 item 的相似度。power 表示是用户的购买力,user_inter 表示用户的兴趣, age 表 示用户的年龄。 reward 的设计: 携程技术沙龙 AI 专场 625 由于最终衡量的是用户的成交和点击率和对话的轮数。因此 reward 的设计主要包括下面 3 个方面: a) 用户的点击的 reward 设置成 1 b) 成交设置成[1 + math.log(price + 1.0) ] c) 其余的设置成 0.1 DRL 的方案的选型: 这里具体的方案,主要采用了 DQN, policy-gradient 和 A3C 的三种方案。 2.2.2 智能服务:基于知识图谱构建与检索模型的技术实践 智能服务的特点:有领域知识的概念,且知识之间的关联性高,并且对精准度要求比较 高。 基于问答型场景的特点,我们在技术选型上采用了知识图谱构建+检索模型相结合的方式 来进行核心匹配模型的设计。 知识图谱的构建我们会从两个角度来进行抽象,一个是实体维度的挖掘,一个是短句维度 进行挖掘,通过在淘宝平台上积累的大量属于以及互联网数据,通过主题模型的方式进行 挖掘、标注与清洗,再通过预设定好的关系进行实体之间关系的定义最终形成知识图谱。 基本的挖掘框架流程如下: 图 12:知识图谱的实体和短语挖掘流程 挖掘构建的知识图谱示例如图 13: 携程技术沙龙 AI 专场 626 图 13:具体的知识图谱的示例 基于知识图谱的匹配模式具备以下几个优点: (1) 在对话结构和流程的设计中支持实体间的上下文会话识别与推理 (2) 通常在一般型问答的准确率相对比较高(当然具备推理型场景的需要特殊的设计,会 有些复杂) 同样也有明显的缺点: (1) 模型构建初期可能会存在数据的松散和覆盖率问题,导致匹配的覆盖率缺失; (2) 对于知识图谱增量维护相比传统的 QA Pair 对知识的维护上的成本会更大一些; 因此我们在阿里小蜜的问答型设计中,还是融入了传统的基于检索模型的对话匹配。 其在线基本流程分为: (1) 提问预处理:分词、指代消解、纠错等基本文本处理流程; (2) 检索召回:通过检索的方式在候选数据中召回可能的匹配候选数据; (3) 计算:通过 Query 结合上下文模型与候选数据进行计算,通过我们采用文本之间的距 离计算方式(余弦相似度、编辑距离)以及分类模型相结合的方式进行计算; (4) 最终根据返回的候选集打分阈值进行最终的产品流程设计; 携程技术沙龙 AI 专场 627 离线流程分为: (1) 知识数据的索引化; (2) 离线文本模型的构建:例如 Term-Weight 计算等; 检索模型整体流程如图 14: 图 14:检索模型的流程图 2.2.3 智能聊天:基于检索模型和深度学习模型相结合的聊天应用 智能聊天的特点:非面向目标,语义意图不明确,通常期待的是语义相关性和渐进性,对 准确率要求相对较低。 面向 open domain 的聊天机器人目前无论在学术界还是在工业界都是一大难题,通常在目 前这个阶段我们有两种方式来做对话设计: 一种是学术界非常火爆的 Deep Learning 生成模型方式,通过 Encoder-Decoder 模型通过 LSTM 的方式进行 Sequence to Sequence 生成,如图 15: 图 15:seq2seq 网络结构图 GenerationModel(生成模型): 携程技术沙龙 AI 专场 628 优点:通过深层语义方式进行答案生成,答案不受语料库规模限制 缺点:模型的可解释性不强,且难以保证一致性和合理性回答 另外一种方式就是通过传统的检索模型的方式来构建语聊的问答匹配。 RetrievalModel(检索模型): 优点:答案在预设的语料库中,可控,匹配模型相对简单,可解释性强 缺点:在一定程度上缺乏一些语义性,且有固定语料库的局限性 因此在阿里小蜜的聊天引擎中,我们结合了两者各自的优势,将两个模型进行了融合形成 了阿里小蜜聊天引擎的核心。先通过传统的检索模型检索出候选集数据,然后通过 Seq2Seq Model 对候选集进行 Rerank,重排序后超过制定的阈值就进行输出,不到阈值就 通过 Seq2Seq Model 进行答案生成,整体流程图 16: 图 16:小蜜的闲聊模块 三、未来技术的发展与展望 目前的人工智能领域任然处在弱人工智能阶段,特别是从感知到认知领域需要提升的空间 还非常大。智能人机交互在面向目标的领域已经可以与实际工业场景紧密结合并产生巨大 价值,随着人工智能技术的不断发展,未来智能人机交互领域的发展还将会有不断的提 升,对于未来技术的发展我们值得期待和展望: 1、数据的不断积累,以及领域知识图谱的不断完善与构建将不断助推智能人机交互的不断 提升; 2、面向任务的垂直细分领域机器人的构建将是之后机器人不断爆发的增长点,open domain 的互动机器人在未来一段时间还需要不断提升与摸索; 携程技术沙龙 AI 专场 629 3、随着分布式计算能力的不断提升,深度学习在席卷了图像、语音等领域后,在 NLP(自 然语言处理)领域将会继续发展,在对话、QA 领域的学术研究将会持续活跃; 在未来随着学术界和工业界的不断结合与积累,期待人工智能电影中的场景早日实现,人 人都能拥有自己的智能“小蜜”。 参考文献: [1] : Huang P S, He X, Gao J, et al. Learningdeep structured semantic models for web search using clickthrough data[C]// ACMInternational Conference on Conference on Information & KnowledgeManagement. ACM, 2013:2333-2338. [2] Minghui Qiu and Feng-Lin Li. MeChat: A Sequence to Sequence andRerank based Chatbot Engine. ACL 2017 [3] Dzmitry Bahdanau, Kyunghyun Cho, and YoshuaBen- gio. 2015. Neural machine translation by jointly learning to align andtranslate. In Proceedings of ICLR 2015 [4]Matthew Henderson. 2015. Machine learning fordialog state tracking: A review. In Proceedings of The First InternationalWorkshop on Machine Learning in Spoken Language Processing. [5] Mnih V, Badia A P, Mirza M, et al.Asynchronous Methods for Deep Reinforcement Learning[J]. 2016 [6] Li J, Monroe W, Ritter A, et al. DeepReinforcement Learning for Dialogue Generation[J]. 2016. [7] Sordoni A, Bengio Y, Nie J Y. Learning concept embeddings for queryexpansion by quantum entropy minimization[C]// Twenty-Eighth AAAI Conference onArtificial Intelligence. AAAI Press, 2014:1586-1592. 携程技术沙龙 AI 专场 630 京东 JIMI 用户未来意图预测技术揭秘 [作者简介]邹波,京东 JIMI 核心算法架构师,致力于 NLP 领域和深度学习方向。目前负责 用户未来意图预测,智能分流,会话结束预测等项目,极大的提高了客服工作效率,同时 也降低人力成本,提升了客户体验。 现场视频:https://v.qq.com/x/page/n0562zcng67.html 随着近年来人工智能技术的发展,Chatbot 聊天机器人越来越普及,随之而来的用户访问 不断增多,如何让 Chatbot 系统在解决用户问题的同时简化用户操作,优化用户与机器人 聊天过程中的体验成为当前难点。 目前的智能问答机器人不仅需要实现智能人机交互(文本、语音等)的全渠道多媒体整合 应用,而且需要各领域内大数据、深度语义理解等前沿技术上的研究与积累,让机器人去 回答用户的同时预测用户接下来的意图,并做对应的个性化处理,因此针对 Chatbot 的用 户未来意图预测技术应运而生。 一、京东 JIMI 及发展现状 1.1 关于 JIMI 京东 JIMI 是由京东自主研发的 Chatbot,通过自然语言处理、深度神经网络、机器学习等 技术,能完成全天候、无限量的用户咨询,涵盖售前咨询、售后服务、闲聊陪伴等环节。 自 2012 年诞生至今,已累计服务数亿用户,覆盖京东 10 亿+的商品,应答准确率 90%以 上,用户满意度高达 80%以上,每月为京东节省上千万人力成本。 1.2 现有技术方案 JIMI 现有技术架构主要由以下模块组成,如图 1 所示: (1)算法:包括纠错、分词、实体识别、知识图谱、词法分析等模块,根据用户输入的问 题,结合领域术语词库和其他语法、语义方面的资源,在解决歧义、指代关系等问题后, 使用深度神经网络技术,提供用户意图的精准理解。 (2)工程:根据业务处理逻辑,判断该问题的答案处理流程,例如答案是闲聊或业务,是 否需要用户登录等。 (3)数据:通过对用户原始数据的挖掘、清洗和聚合等,实现对客服领域知识的储备,并 对现有数据做可视化处理。 携程技术沙龙 AI 专场 631 图 1:Chatbot 系统架构图 1.3 现有方案的缺点 传统 Chatbot 只能根据用户的当前问题,给出对应的答案,类似于一问一答的形式。对于 用户在聊天过程中接下来的意图,没有预测功能。如此以来用户每次都必须完整输入自己 想问的问题,才能获得相关答案。这种方式比较费时,用户体验也不是特别好。 本文接下来会介绍一种基于用户未来意图预测的方法,对用户的聊天过程做实时分析,根 据当前及历史问题,智能预测用户接下来的意图,提升用户的聊天体验。 二、预测未来意图技术方案 2.1 应用场景 基于现有技术存在的问题和缺陷,我们提出了一种智能预测用户下一个意图的方法。该方 法基于用户目前订单、购物车状态等账号信息以及历史聊天内容,智能预测用户接下来最 可能问的问题。它主要会在以下两种场景下使用: 1)用户开始咨询前预测 如何在用户进入 JIMI 后还未咨询前,提前预测用户可能会问到的问题,并将其直接展示给 用户供用户点选,提升用户体验,需要解决两个技术问题,一是如何获取用户可能会问的 标准问题,二是如何做到个性化地对不同的用户推送不同的问题。 在具体实现上,采用人工去整理就存在人力成本高、问题更新不及时的情况,因此我们采 携程技术沙龙 AI 专场 632 用无监督的聚类方式得到用户可能问的标准问题,再通过线上试验,先随机出这些问题, 收集用户点击作为分类标签,最后用分类的思想去解决它。 具体技术实现如下,如图 2 所示,首先按热门 SKU 的维度收集问题,包括用户在 JIMI/咚 咚发送的信息,以及单品页购买中咨询的问题。这些原始问题不能直接作为标准问题进行 使用,所以需要人工进行一次过滤,由于数据量非常庞大,这里采用 Logistic Regression 训练一个语料过滤模型,用于数据清洗。接着对这些问题做切词,word2vec 训练词向量, 进而得到句子向量,最终用 K-means 聚类的方法,找到最大的前 20 个 Cluster,选出现次 数最多的问题作为标准问题。 图 2:用户进入咨询前预测 系统上线后,先随机出这些问题,然后根据用户点击行为确定样本的标签,再收集用户的订 单、服务单、实时浏览数据、以及画像数据作为样本,最终训练一个用于用户开始咨询前 的未来意图预测分类模型。当用户再来咨询时,根据分类模型给出用户最可能问到的前 3 个问题,供其点选。 2)用户咨询过程中实时预测 对于用户咨询过程中的未来意图预测,如图 3 所示,系统会在用户说每句话时,实时预测 用户下一意图,并将预测的 TOP5 用户意图展示在前端界面。如果用户觉得预测准确,可 直接点击该问题获取答案,不用自己手动输入问题,从而提升用户体验。 携程技术沙龙 AI 专场 633 图 3:用户咨询过程中实时意图预测 2.2 未来意图预测流程 用户咨询过程中未来意图预测流程包括预处理、模型预测,数据记录三大模块。如图 4 所 示: 携程技术沙龙 AI 专场 634 图 4:用户咨询过程中未来意图预测模块内部流程图 各模块的作用如下: 预处理:预处理模块主要做一些必要入参的判断,比如判断输入的用户 ask 是否合法,以 及对于用户提问小于 2 句的情况,不做未来意图预测处理。用户提问大于 2 句才会继续往 下走到模型预测模块。 模型预测:通过模型计算用户下一个可能的问题概率,如果预测值低于当前设置的阀值则 不做推送,高于阈值才会继续往下走到数据记录模块。 数据记录:负责系统日志记录,比如记录下每句话具体推送了哪些分类,方便系统上线后 模型调优。 接下来,详细介绍模型预测子模块。该模块通过模型分类的方法,将用户问题对应到不同 分类,并实时计算用户下一问题的概率。具体技术方案如下: 1)样本构造 首先,收集用户和客服的聊天日志信息,我们可以根据这些海量信息,发现用户当前问题 与下一个问题的联系。 比如用户进入咨询首先发送“你好”,然后说“这个商品有货吗?”,紧接着问“有优惠 携程技术沙龙 AI 专场 635 吗?”,最后问“现在下单,什么时候能送到?”。这时我们构造样本就需要把前三句话拼 在一起,构造出来这样的样本“你好这个商品有货吗?有优惠吗?” 2)标签构造 上面这个样本的标签就是第四句话“现在下单,什么时候能送到?”所对应的分类。 如何确定分类?用人工审核的方式,将所有用户的问题都看一遍,并将每个问题对应到一 个具体的分类。比如用户问“我的商品有货吗?”或者“还有货吗?”都会被分到“是否 有货”这个分类,标记“分类 1”,以此类推。 3)标签选择 推送给用户的问题,最好是用户常问的问题,而不是一些长尾问题,这样可以提升推送的 准确率。 统计最近 1 年的聊天日志,将所有用户每句话对应到一个分类标签,计算出 TOP10 的分类 标签,主动推送的分类就限定在这 TOP10 之中。最终构造出的样本和标签信息,如图 5。 图 5:构造样本和标签 样本构造的总体思路:  从最近 1 年的聊天日志取出用户原始问题  将用户的问题分类,每个用户问题对应一个类别标签  每通会话包含 N 个用户问题,其中前 N-1 个问题拼起来作为样本,第 N 个问题的分 类,作为该样本标签  最终取 TOP10 的标签分类,保证预测结果能够覆盖用户的高频问题 4)模型训练: 深度学习 CNN 模型,可用于求解一个分类问题,将用户的问题映射到一个具体的分类。 最终在算法选型上,我们采用深度学习 CNN 模型,其中模型参数: 携程技术沙龙 AI 专场 636  词向量采用 100 维  每个样本限定 30 个字以内,超出 30 截断,不足 30 补充随机向量  单层 CNN 网络,第一层卷积核大小 3*50 5)模型效果 最终模型效果的统计,我们通过建立 BaseLine 与模型对比的方式来度量。BaseLine 的建立 思路如下:针对当前分类 X,基于历史数据统计,给出最高频的下一分类 Y 三、结语 经线上验证,用户未来意图预测技术已经能优化用户咨询效率和咨询体验,让机器人不仅 “懂你所问”,更“知你所想”。后续基于不断优化提高的自然语言理解能力和深度学习, 对用户未来意图预测会越来越准确,让用户体验更智能的机器人。 携程技术沙龙 AI 专场 637 助理来也胡一川:深度学习在智能助理中的应用 [作者简介]胡一川,来也联合创始人和 CTO。来也专注于智能对话技术,让每个人拥有助 理。此前,胡一川联合创立了影视推荐引擎"今晚看啥"并被百度收购,后加入百度任资深 架构师。本科和硕士毕业于清华大学,博士毕业于宾夕法尼亚大学。 现场视频:https://v.qq.com/x/page/i0562oqzqsk.html 一、什么是智能助理 随着智能手机和移动互联网的普及,越来越多原来发生在线下的交互场景,逐渐从线下转 移到线上。人们也开始习惯通过在线沟通的方式来获取各种服务:让秘书安排出差的机票 和酒店,向英语老师咨询学习中的问题,找旅行达人制定旅游计划等等。类似这样的场 景,今天都逐渐从面对面或电话沟通,转移到线上沟通。 因为沟通从线下变为线上,大量的数据能够被沉淀下来,基于数据我们可以通过机器学习 等方法来辅助人提升效率,甚至在某些场景下替代人,从而实现智能助理。我们给智能助 理的定义是:基于人工智能技术,通过理解语音或文本形式的自然语言来满足用户需求的 软件应用或平台。 那么智能助理是不是就是智能客服呢?我们认为智能客服是智能助理的一种形态,但智能 助理比智能客服有更深层次的意义和更广泛的应用。与智能客服不同,智能助理有以下 3 个特点: 1.更主动的双向交互:在客服场景下,通常是用户主动联系客服,客服被动响应。而在助 携程技术沙龙 AI 专场 638 理场景下,助理和用户的交互是双向的,助理可以主动联系用户,在适合的时候主动为用 户提供合适的服务。 2.更长期的伙伴关系:在助理场景下,用户和助理的关系是长期,用户可以在长时间内通 过同一个助理持续获得专属化的服务。相反,在客服场景下,用户和客服之间的关系往往 是短暂的,双方的连接只在服务的那一刻建立,服务完成后即断开。 3.更丰富的价值场景:因为双向的沟通与长期的关系,助理能够为用户提供更丰富和更有 价值的服务。这些服务不仅仅限于售后,还包括售前咨询,甚至一些专业化的服务也可以 通过在线助理的方式来完成。 通过智能助理来获得信息、商品和服务将成为趋势。那么,这个趋势会首先发生在哪些行 业中?我们通过两个维度来思考这个问题。第一个维度是在线交互需求度,即该行业中在 线交互的需求强不强,场景多不多。第二个维度是领域知识专业度,即这个行业的领域知 识是否比较复杂,用户的决策过程是否需要借助外部知识。 只有当这两个维度都比较强的时候,以自然语言对话为主要交互方式的助理产品才能给用 户带来比较高的价值。比如,在线秘书是一个非常典型的例子。今天,如果我有一个秘 书,大部分时候我不需要和秘书见面,通过在线沟通的方式就可以把我想让他做的事情交 代清楚了。再比如说,母婴、教育、旅游等行业都是非常典型的在线交互需求度较强和领 域知识专业度较高的领域,适合智能助理的落地。可以预见,随着越来越多线下交互场景 迁移到线上,智能助理会在更多的行业中成为一种主流的产品形态。 二、基于深度学习的自然语言处理框架 在线交互的场景会产生大量自然语言对话数据,基于这些数据我们可以训练机器学习模 型,让机器具备一定自然语言处理和理解的能力,从而打造智能助理。 携程技术沙龙 AI 专场 639 自然语言处理在智能助理产品的各个环节中都有应用,从分词、词性标注,到意图识别、 实体抽取,再到问答、对话等。过去两年,学术界和工业界开始将深度学习应用在自然语 言处理任务上,取得了很多不错的进展。在某些特定任务下,基于深度学习的方法明显优 于基于传统机器学习模型的方法。因此,本节我们主要介绍深度学习技术在智能助理中的 应用。 上面提到自然语言处理中有很多种不同的任务,但从机器学习模型的角度来说这些任务都 有相同之处:模型的输入都是自然语言文本,输出都是某些预测结果,只不过在不同任务 下模型需要预测的东西不一样。比如,在意图识别中,模型需要预测的是一段文本表达的 用户意图;在实体抽取中,模型需要预测的是一段文本中的每个字或词所对应的实体;在 问答或对话中,模型需要预测的是用户的问题和机器的回答的匹配度。虽然预测的内容不 同,基于深度学习的自然语言处理框架可以总结为以下 4 步: 1,Embed。这一步所做的事情是将待处理文本中词或字用分布式向量的方式表示,作为下 面步骤的输入。这些向量又称为词向量或字向量,可以事先训练得到,也可以先初始化成 随机向量,然后在训练当前任务的过程中调整。当然,这一步中也可以将其他对实现当前 任务有价值的信息作为输入,如用户行为等。 2,Encode。当我们把一句话用词向量表示后,这些词向量并不能表示这句话的语义,因 为一句话的意思并不等于其包含的词的意思的简单组合。因此,Encode 这一步所做的主 要工作是对整段文本进行编码,编码的过程考虑到每个词和它上下文之间的关系。 我们通 常使用卷积神经网络(CNN)或者循环神经网络(RNN)对文本进行编码,这样可以充分 利用词与词之间的关系。编码的输出是一个新的向量或矩阵,能更好地表征整段文本。 3,Attend。这个步骤又称为注意力机制(Attention Mechanism), 其主要思想是通过训 练让模型关注在文本中能够解决当前任务的最重要的部分。用通俗的话来说,注意力机制 携程技术沙龙 AI 专场 640 就是给文本“划重点”,从而提升模型预测的效果。 4,Predict。这一步目标非常清晰,即将上一步中的输出通过一个网络完成当前的预测任 务,通常使用到的网络模型是全连接的前馈神经网络。根据预测任务的不同,预测结果可 以是一个标签的概率、一个实数值或者一个向量等。 下面我们来看一下,基于上述框架使用深度学习能够应用在智能助理的哪些场景中,解决 哪些具体问题。 三、深度学习的应用:意图识别 第一个场景是意图识别。意图识别的作用是根据自然语言判断用户的意图。例如,在助理 来也的场景里,当用户通过自然语言发起一个需求时,用户的意图是问天气、订机票还是 其他,是意图识别模型需要解决的问题。上面介绍的框架能够非常好的应用在解决意图识 别问题上。 首先,意图识别模型的底层是一个双向的 LSTM 网络,即一种特殊的循环神经网络,该网 络的输入是经过向量化表示(Embed)的用户消息,该网络的作用是对用户的消息进行编 码(Encode),输出是若干个隐向量。编码后的结果经过一个注意力层(Attend),使模型 学习到不同词对应的隐向量对于预测结果的权重。最终,经过注意力层加权后的隐向量经 过 Softmax 层来预测(Predict)用户消息对应意图的概率。 和基于传统机器学习模型的方法相比,该方案最大的优点是完全靠数据驱动,无需人工进 行特征工程,能最大化的利用数据本身蕴含的信息来进行意图预测。同时,基于深度学习 的方法效果也明显优于传统的方法。 在助理来也的产品中,我们在 20 多类的意图识别问题上对不同的方法进行了对比。最 携程技术沙龙 AI 专场 641 初,在缺乏数据的情况下,我们使用传统基于规则的方法,准确率只有 70%左右。随着数 据的积累,我们切换到基于传统机器学习的方法,准确率迅速提升到 90%。但是,当传统 机器学习模型准确率到达 90%之后,我们发现很难进一步提升,因为传统的方法依赖于特 征提取,怎么选择和构造特征直接决定了模型的效果。随着特征数的增加,构造新的特征 变得更难,而增加新的特征对模型效果的影响也越小。 为了解决这些问题,我们切换到基于深度学习的模型上,不依赖特征提取,完全靠数据驱 动,效果明显比传统方法好,准确率达到 96%以上。当然,在实际使用过程中还会遇到很 多其他的挑战,比如用户的意图不仅仅和当前用户消息有关,可能和用户的历史消息甚至 历史行为有关。基于此,我们可以在模型中引入更多的输入,如历史消息、历史行为等, 来进一步提升意图识别的准确率。 四、深度学习的应用:知识挖掘 接下来介绍深度学习在知识挖掘上的应用。在智能助理的场景中,用户会问各种各样和该 领域相关的问题,每个问题都有特定的答案,我们把这些问题和答案称为领域知识。要让 智能助理具备自动问答的能力,首先需要把这些知识从非结构化的对话语料中挖掘出来, 作为自动问答模型的训练数据。具体而言,知识挖掘的目标是从自然语言对话语料中将用 户问题挖掘出来,并将相同语义的问题归到同一个知识点下。下面是母婴助理场景中两个 例子。 表达方式不同,但属于同一个知识点: 刚出生 1 个多月的小孩能晒太阳吗? 新生儿是不是要满月才可以晒太阳? 表达方式接近,但属于不同的知识点: 新生儿晒太阳,每次多久比较合适? 新生儿晒太阳,多大开始比较合适? 在上面的两个例子中,知识挖掘需要将第一个例子中的两句话归为同一个知识点(宝宝多 大可以晒太阳),而将第二个例子中的两句话归为不同的知识点(宝宝每次晒太阳时间 vs 宝宝多大可以晒太阳)。因此,知识挖掘的主要难点是对文本进行语义表示,然后进行聚 类。 传统的知识挖掘方法使用基于词向量的无监督聚类。具体的做法是,对于任意两句文本, 使用它们包含的词和词向量来计算文本间的距离,再基于文本间的距离来实现无监督的聚 类。这种方法有两个比较明显的缺陷:1)基于词向量来计算文本间的距离,不能很好的反 映文本的语义相似度;2)使用无监督的聚类,很难确定类的数目,导致结果聚类结果不可 控。 携程技术沙龙 AI 专场 642 针对这两个问题,我们采用基于深度学习的方法,在词向量的基础上训练句向量,将无监 督的方法和有监督的方法结合起来。具体而言,我们首先通过传统的方法挖掘出一部分知 识点,人工审核后进入知识库,我们称之为种子知识库。在种子知识库的基础上,我们能 够构造训练数据:同一知识点下的问题对作为正样本,不同知识点下的问题对作为负样 本。基于上述训练数据,我们能训练出一个针对问题对的语义匹配的模型。这个模型和上 面提到的框架完全一致,也包括 Embed、Encode、Attend、Predict 这 4 个步骤。 模型训练好之后,我们将其中的编码器(Encoder) 单独拿出来使用,对语料中的其他问 题进行编码,编码结果可以认为是句向量,能够表征句子的语义。基于句向量,我们再做 基于聚类,效果和效率比基于词向量的方法都会有很大的提升。 五、深度学习的应用:自动问答 最后再来看一看深度学习在自动问答中的应用。自动问答模型的主要目标是针对一个用户 的问题,返回知识库中最适合回答该问题的知识点。传统的自动问答使用基于检索的方 法,将用户问题作为输入去检索知识库,并返回相关性最高的若干个结果。基于检索的方 法存在两个问题:1)检索是基于关键词的,检索相关性不能代表语义相关性;2)实际场 景中的问答通常和上下文有关,在这种情况下仅基于单句用户消息的检索无法返回合适的 结果。针对这个问题,我们采用基于上下文检索加深度学习匹配排序的方法。下面详细介 绍。 携程技术沙龙 AI 专场 643 首先,我们从用户当前消息和上文中抽取关键词,去知识库或历史语料中进行检索,返回 若干个候选回复。因为检索关键词不仅来自于当前用户消息,也来自当前对话的上文,检 索结果会既包含和当前消息相关的回复,也包含和上文历史消息相关的回复。 接下来,这些候选回复逐一输入到一个基于深度学习的文本匹配模型中,模型返回每个候 选回复和当前对话上文的语义匹配度。最后,根据匹配模型返回的分数,系统返回分数最 高的若干个候选回复。 深度匹配模型使用 CNN 对一个候选回复与当前用户消息以及历史消息序列进行匹配,最 终计算出候选回复和整个对话上文的匹配分数。模型的训练数据来自于历史语料,将历史 对话切割成若干个“上文”和“真实回复”的配对作为正样本,将“上文”和“随机回 复”配对作为负样本。基于此,该匹配模型能够充分利用历史数据,同时考虑到上下文关 系,实现候选回复和上文历史消息的匹配。 这个模型也完全符合我们前面介绍到的框架:候选回复和若干条上文消息的匹配可以看作 是 Encode 步骤,而若干个匹配后的向量进行池化等操作可以认为是 Attend 步骤,最终 输出语义匹配度则是 Predict 步骤。 六、智能助理在行业中的落地 前面提到,不同行业的在线交互需求度和领域知识专业度都有所不同。因此,靠数据驱动 的智能助理产品更适合在不同行业中以行业助理的形态落地,而不是以通用助理的形态落 地。当我们聚焦行业后,能够积累足够多的领域对话数据,打造更加智能、用户体验更好 的智能助理产品。 基于这个思路,我们首先针对在线秘书行业打造了一款助理产品“助理来也”。用户可以通 过自然语言的方式获取 20 多项和工作、生活相关的服务,包括日程提醒、打车、咖啡、 携程技术沙龙 AI 专场 644 跑腿等。目前,“助理来也”是微信平台上深受欢迎的助理产品,为超过 300 万用户提供 一站式的在线助理服务。在这个过程中,我们积累了大量的交互数据,将深度学习技术成 功的应用在意图识别、实体抽取、问答、对话等各环节中,提升模型的效果和产品的体 验。除此之外,我们也通过“吾来”输出语义、问答、对话等技术,帮助各领域企业客户 打造行业助理。目前已经在母婴、汽车等行业的标杆企业实现商业化落地。 七、结束语 最后我们进行总结。首先,随着移动互联网的普及和物联网时代的来临,基于自然交互的 智能助理产品将逐渐成为主流。不同于智能客服,智能助理更加强调双向的沟通,长期的 关系和个性化的服务。在这个场景下,基于数据驱动我们可以使用深度学习等技术提升语 义理解、问答、对话等模型的效果。现阶段,针对行业、针对具体场景的智能助理产品更 有用户价值和商业价值。 参考文献 1,Honnibal M. Embed, Encode, Attend, Predict: The NewDeep Learning Formula for State- of-the-art NLP Models. Available at https://explosion.ai/blog/deep-learning-formula-nlp, 2017. 2,Conneau A, Kiela D, Schwenk H, et al.Supervised Learning of Universal Sentence Representations from Natural LanguageInference Data. In Proc. EMNLP, 2017. 3,Wu Y, Wu W, Xing C, et al. SequentialMatching Network: A New Architecture for Multi- turn Response Selection inRetrieval-Based Chatbots. In Proc. ACL, 2017. 645 携程技术中心微信公号(ID:ctriptech) 携程技术中心官方账号,分享来自携程技术人的一手干货文章,发起线上线下技术活动, 发布热招岗位信息,和技术圈小伙伴一起学习成长~ 携程技术中心微信公号需要你的帮助~ 把你想看的内容告诉我们,我们给你一个最精彩的技术公号。 反馈戳这里:http://ctriptech.mikecrm.com/nbRt3Op

下载文档,方便阅读与编辑

文档的实际排版效果,会与网站的显示效果略有不同!!

需要 10 金币 [ 分享文档获得金币 ] 3 人已下载

下载文档

相关文档