Extra Cookie

Yet Another Programmer's Blog

关于数据采集和用户行为分析平台的一些问题

最近要参加一个关于数据埋点和分析的线上讨论,这两天总结了对一些问题的思考。

为什么企业需要一套完善的用户行为埋点和分析平台?

一个互联网产品从萌芽到发展壮大,离不开对用户行为的深度洞察。

产品初创期间,需要分析天使用户的行为来改进产品,甚至从用户行为中得到新的思路或发现来调整产品方向;产品 growth 过程,通过对用户行为的多角度(多维)分析、对用户群体的划分以及相应行为特征的分析和比较,来指导产品设计、运营活动,并对市场渠道效果进行评估。

配合上 A/B 试验平台,可以加速产品的迭代,更快得到用户的真实反馈。同时,这些数据沉淀下来,对业务的数据仓库建设、数据智能应用等方面也能起到促进作用,比如做实时推荐,需要能更快获得用户尽可能多且明细的行为数据;做用户分类、意愿预测等机器学习业务,需要清洗过的规范化、结构化的数据做 training。

要能做用户行为的分析,就需要有一套用户行为数据采集、传输、处理、分析的基础设施,而埋点和分析平台就是在做这件事。业界大多产品都是通过嵌入到多个终端的 SDK 来采集用户行为数据,而后续的传输、处理等过程对需求方是透明的,这样可以以很低的成本,把数据的采集、清洗、沉淀工作做掉,为企业节省成本,提升数据驱动的效率。

在分析平台上,用户的行为定义会通过特定 Event 来标识,比如 “buttonClick”, “playMusic” 等。通常这些事件,是开发人员通过调用 SDK 提供的 API 来设置的,除了确定事件的名称外,还可以加入分析需要的自定义参数和取值,这个过程就是“埋点”工作。当然,还有一些工具/产品支持可视化埋点,这种方式不需要开发介入埋点,SDK 会自动去采集用户在各个终端上的行为。

代码埋点、可视化埋点和无埋点有哪些区别,在使用过程中该如何选择?

分析平台通常会提供各端的数据采集 SDK,代码埋点是产品开发者通过调用这些 SDK 提供的一些 API 来记录用户行为的方式,及俗称的“埋点”或者“打点”。

trackEvent("buttonClick")

可视化埋点是指开发人员除集成采集 SDK 外,不需要额外去写埋点代码,而是由业务人员通过访问分析平台的 圈选 功能来“圈”出需要对用户行为进行捕捉的控件,并给出事件命名。圈选完毕后,这些配置会同步到各个用户的终端上,由采集 SDK 按照圈选的配置自动进行用户行为数据的采集和发送。

无埋点是指开发人员集成采集 SDK 后,SDK 便直接开始捕捉和监测用户在应用里的所有行为,并全部发送到分析平台,不需要开发人员添加额外代码。在分析时,业务人员通过分析平台的圈选功能来选出自己关注的用户行为,并给出事件命名。之后便可以对特定用户行为(事件)进行多维分析了。

可视化埋点和无埋点比较像,都不需要开发人员手工加代码,也都需要业务人员进行所关注的用户行为的圈选。两者最大的不同是在用户终端的表现上,可视化埋点只采集业务人员关注的用户行为数据,而无埋点是会采集所有用户的行为数据,通常情况下数据量后者比前者大很多。

也正是由于无埋点默认采集所有用户行为数据,它能够做到事件的回溯分析,即在业务人员新定义(圈选)事件后,就能去分析这个事件在前面一两个月的数据情况,这也是可视化、代码埋点支持不了的。但带来的问题就是采集所有数据对应用的侵入会有些大,也会增大用户端采集的数据量。当然,可以通过一些策略,比如 Wi-Fi 下才发缓解这些问题。

无埋点和可视化埋点很大一个缺陷在于它们都是通过采集 SDK 去监测应用上控件的触发事件(用户对控件的操作),当产品 UI 在版本升级过程发生变动,或者产品做了大的改版,一些行为的“埋点”会发生丢失。如控件ID发生变化,而圈选的配置没变,导致数据采集不到;或者和业务的实际需要发生不一致的变动,比如圈选控件的作用发生了变化,但圈选配置没改;这些问题会导致对产品某些方面的分析出现差错,往往查起来还比较麻烦,在技术上完全解决也比较困难。

另外,可视化埋点和无埋点都针对的是客户端数据采集,一些用户行为数据在客户端是采集不到的,或者客户端采集的精准度不够,比如支付,因为支付成功的判断绝大多数场景都是在服务端做的,所以在客户端做支付行为的埋点,误差很大,这个时候就需要在服务端进行埋点。

我的建议是,在产品初期,产品形态还不太稳定、分析的复杂度还比较低的阶段,采用无埋点或者可视化埋点,更快去做埋点,否则频繁的产品改动,会让开发人员大量时间花在琐碎的埋点代码维护上面。产品进入稳定期后,尽量采用代码埋点方式,可以保证事件模型是稳定的,便于长期的数据监控、分析和数据沉淀。

如何进行数据埋点方案及规范的定义,以及后续怎么进行维护和管理?

一个互联网产品业务数据驱动的 workflow 往往是这样的:

  1. 定义产品的阶段性目标;
  2. 规划和定义指标,包括产品、运营、市场的各项目标;
  3. 产品、运营等业务人员确定数据埋点需求;
  4. 开发人员进行埋点以及数据的上报等开发工作;
  5. 数据开发人员进行数据的清洗、宽表建设、指标计算等工作;
  6. 业务人员分析数据、发现产品问题或潜在机会;
  7. 继续下一阶段的产品、运营、市场等的改进工作。

用户行为分析平台的目标就是将其中 4-6 阶段的工作变得简单和自动化,把开发人员解放出来去做更多对业务有价值的工作。而 1-3 部分的工作,看起来不复杂,基于业务现状去定义指标,排出埋点需求,和开发人员确认好就完成了。

但这块从实践上来看,很多企业或者业务都做的不够好。比如定义的事件数量迅速膨胀,一段时间后,团队可能大部分人都不知道某些埋点是做什么的,开发人员也不好删掉,就一直存在着,可能早已失去了业务价值;或者业务人员定义了埋点需求,但开发人员埋点做错了,彼此都没发现,导致分析过程出现错误解读;又或者上线了才发现埋点的参数或者位置不对,但又必须得等到下一次发版本才能解决。

这块有几件事情可以做:

  • 指标管理系统,用来维护指标依赖的数据表、字段以及计算方式,来统一开发、分析和解读过程的口径。
  • 埋点管理系统,用来管理埋点的元数据,包括事件 Event 的命名、自定义字段含义和特定取值等规范定义,埋点在产品端的位置或触发场景,埋点工作流等,作为业务人员、开发者、分析师沟通的桥梁和基准。
  • 埋点测试和校验系统,提供 debug 工具方便开发人员快速进行埋点调试,以及使用事件定义的规范要求,在线上对埋点数据进行校验,尽早发现不符合规范的数据,提高埋点工作的效率和准确性。

如何做好埋点工作和研发的协调和落地?

在实践中,很多开发人员不太愿意做“埋点”的工作,觉得很琐碎,而且随着产品的发展,包袱有时候会越来越大,维护的工作量不小。

要让埋点工作在研发比较好的落地,最能提升的地方还是在于如何简化开发人员的工作,包括开发成本和沟通成本。

采集 SDK 应该尽量简化 API,能自动做的就不要让开发人员来做,比如应用生命周期的检测、PageView 的采集、甚至对一些企业内部组件化框架的支持,尽可能减少开发人员接入分析平台需要添加的代码量。

有完善的埋点管理系统,这样研发端可以依据进行开发,减少“口口相传”带来的低效和返工,也能统一口径和进度流程。

有高效易用的埋点测试、校验系统,开发人员可以快速进行埋点 debug,提高开发效率,也能让业务方尽早介入需求校验,而不是等应用真正发布后才去校验,去发现问题。

当然,最好能和开发人员持续分享数据是如何促进业务的发展,让大家明白这些工作的价值,才能更重视,更认真对待这部份工作。

埋点数据采集与企业数据资产建设怎样更好的合作?

用户行为分析平台在建设时,数据端会包含如下能力:

  • 数据接入,要支持客户端、Web、服务端等多终端的数据采集,如 iOS、Android、微信小程序等,以及各种数据源甚至三方服务的数据适配。
  • 数据传输,在用户规模和数据规模增长过程中,要能保证数据传输服务的高可用、以及采集数据在传输过程的及时性。
  • 数据建模/存储,要能实时的进行数据清洗、建模和存储落地。

这些能力,在互联网业务的数据资产建设过程中,尤其是用户、流量、产品相关领域,能起到基础设施的作用。规范的数据采集,加上高效的传输、建模能力,是企业业务数据资产有效建设的前提。

建模后的数据,可以作为数据仓库底层(ODS 层)的宽表,和企业的其他业务数据整合,共同完善企业的数据资产建设。

另一方面,这些用户端的结构化数据,加上实时建模和开放的能力,和机器学习算法结合起来,无论是个性化推荐,还是精准营销,又或是银行、电商的风控,都可以发挥很大威力,为企业的智能驱动业务做好数据积累,扫清障碍。

企业在数据建设的过程中的产出,也可以扩充以 PaaS 提供服务的用户行为分析平台的能力,让企业在平台上可以做更多的事情,如 CRM、推送、实时推荐等。

拿 DMP (用户画像)建设举个例子,

企业在建设自己的 DMP 库的过程中,常常会从常规的人口属性等准静态类标签,以及像消费能力等从自身业务积累或三方合作得到的通用类标签入手。这些标签往往是泛业务的,针对具体业务而言,很多时候会需要用户画像标签更贴近业务,比如电商业务场景下的母婴用户、电子产品发烧友、化妆品品牌喜好用户等。这些标签和用户的发掘,需要对用户的行为进行深度分析来获取,这个工作便可以借助用户行为分析平台的能力,如基于用户行为模式和用户业务属性对用户进行分群分析和比较,来发现和挖掘有价值的用户标签。

另一方面,用户画像的数据,也可以和分析平台进行整合和集成,提升平台各分析模型对不同用户群的洞见能力,让分析和指标的比较更有针对性,提升数据对业务的促进能力。

埋点及分析平台和 A/B 试验平台如何更好的互相促进?

A/B 测试产品是通过提供专业高效的试验平台,帮助产品进行产品决策的验证和分析。常规使用流程如下:

接入 SDK -> 创建试验版本 -> 设置变量、以及优化指标 -> 调节试验流量 -> 运行试验 -> 实时监控数据进行效果评估 -> 正式发布

试验平台和分析平台的 SDK 在很多功能上是重合的,在 SDK 实现上可以整合,减少业务应用接入太多 SDK 的负担。

在数据采集、建模、分析层面,分析平台可以做为 A/B 试验平台后端数据的承载,优化指标的效果评估就能覆盖用户的全量行为,无需业务及开发人员维护多个工具带来的重复埋点定义和开发工作。另外,在分析平台积累的很多分析模型和指标,在 A/B 试验平台直接可以选取使用,无需在试验平台再进行设置,除减少业务人员工作外,还能保证统计口径的一致。

反过来,A/B 试验平台的一些对比试验,以及特定灰度发布的用户群,也能整合到分析平台,通过分群分析能力,将这些群体应用到各个分析模型进行针对性的分析,甚至试验结束后,也能持续对这些用户进行追踪和分析,更好的洞察用户。

如何打通产品多端的埋点数据?

目前大多数用户行为分析产品都会通过 SDK 支持 iOS、Android、Web 三个端的数据采集,还有些产品覆盖的更广,支持 PC、微信小程序、服务端、甚至直接基于 HTTP API 进行数据采集。

在分析如何打通多端用户数据前,我想先谈下单个终端的用户标识问题,毕竟,如果单个端的用户标识都不稳定,那多端用户数据打通也就失去了意义。

现在的分析产品在一般情况下,移动端会通过 SDK 生成唯一 ID 来标识用户/设备。移动化发展早期,很多采集工具用过 mac address、IDFA、android_id、IMEI 等从移动操作系统可以获取的设备软硬件信息来标识设备,但随着操作系统的发展,很多信息获取接口要么被封禁,要么已经失去了精准性。反倒是一开始就通过自己生成的 ID 来标识用户的工具,受到的影响不大,基本保持了用户/设备标识的稳定。

但这种方式有个问题,在用户卸载、重装或者刷机后,ID 信息会丢失,导致生成新的用户/设备 ID。这个问题,可以通过 ID Mapping 技术来解决:

分析平台对每个用户生成一个虚拟 ID,对同一个用户的多个设备和帐号进行映射,并绑定起来。

  • 可以通过操作系统提供的一些稳定性稍差,但短时间还比较稳定的指标,如 iOS 的 IDFA,来做 mapping。
  • 借助分析产品的应用覆盖率,如用户是应用 A 和 B 的用户,卸载并重新安装 B 应用后,可以通过应用 A 的 ID 修复应用 B 的。
  • 通过引入产品用户帐号体系,来做绑定,这种方式稳定性最强,但非登录匿名用户的问题不好解决。
  • 通过 IP、Wi-Fi 信息、机器型号、甚至地理位置进行 mapping,这种方式需要用户授权更多数据获取权限,虽然是近似匹配,但当信息足够多且发散(信息熵足够大)时,也可以起到统一标识的作用。

通过这个虚拟 ID 实质上就打通了产品的多端数据。实践中,ID Mapping 体系的建设工作量不小,Mapping 后用户标识如果需要发生调整,在基于事件的分析产品上需要对老数据进行重写,比较复杂。所以对于一些强帐号体系的产品,可以退化到只用用户帐号来做关联,只有非登录匿名用户才用设备 ID 来标识,这往往是性价比比较高的方案。

再引申下,在多端数据打通问题的讨论中,经常会提到用户来源归因的问题。

比如,产品做推广,使用了百度 SEM、广点通、应用市场等渠道,想知道各个渠道有多少用户激活了,以及后续使用情况如何。

为了解决这个问题,支持营销效果评估的分析平台会要求产品在平台上生成推广链接进行投放。用户在点击链接时,会从分析平台的域下做跳转再到目标页,这样就可以借助浏览器的 cookie 机制进行匹配,来对用户来源进行归因,但这种方式在移动端上面的表现不太好(iOS 已经取消了 SFSafariViewController 多应用共享 cookie 的支持)。除此之外,也可以采用 ID Mapping 提到的近似匹配技术,很多厂商声称的设备指纹技术大多也是这种,不太准,但定性分析是可以的。

一些做移动业务比较多的推广渠道,支持设备 ID 的回传功能来方便产品归因问题的解决。产品方在投放链接的时候,遵照特定格式即可。

https://xxx.com/aaaafD?idfa=__IDFA__&imei=__IMEI__

渠道在用户点击广告链接后,会把设备 ID 如 IDFA 或 IMEI 加到链接的内容里面,用户激活后便可以通过相应 ID 匹配来归因。

Readings in Database Systems - Interactive Analytics

最近在看 Stonebraker“Readings in Database Systems”, 发觉开拓了很多思路。

这么多年自己一直在从事大数据方面的工作,但除了翻过数据挖掘算法和分布式系统设计方面的论文外,完全没想过去翻翻数据库相关的论文看。现在想想,其实大数据和数据库两者很多需求和场景是一致的,要解决的问题,没准学术界很多年前就已经有方案了。

这篇文章主要是 “Interactive Analytics” 相关部分。

What is Interactive Analytics

假如你是一家电商公司的分析师,如果有 100 万用户原始交易数据打印出来摆在你面前,让你去分析这些数据的意义,你会怎么做?

如果这十万条数据给我,我估计是看不出什么东西出来。而且我相信每个人,也是如此,因为人的认知是有 bug 的,比如不能直接处理大量原始数据。

那该怎么办?

我们需要把数据通过一些方式做提炼,变成小的结果集,或者以可视化的形式展现出来。用过 SQL 的人也许会想到 Group by 语句,是的,往往通过 Group by 做 aggregation 后的数据,会好理解很多。

大数据不只是数据量超大,更在于能从大量数据里面发现价值。

而 “Interactive Analytics” 指的就是这个过程,但加了个前提:这个过程必须能在较短的时间内完成,哪怕甚至来不及遍历所有需要的原始数据。

当数据量超大的时候,这个前提对每个数据系统都是一个很大的挑战。

Ideas

那怎么让一个查询请求的执行过程比直接遍历所有依赖的数据还快呢?

结论,显而易见,只能不去遍历所有的依赖数据,能有这样的方案,那问题也就迎刃而解了。

目前靠谱的方案有两种:

  1. Precomputing,如果预先把查询请求依赖的相关数据都做了一定的 “提炼”,便可大大减少查询需要去遍历的数据。
  2. Sampling,可以对数据进行取样,每次查询请求都只遍历取样后的数据,这样遍历数据也可以大大减少。

“Red Book” 给出了四篇参考文献,12是关于 Precomputing 的,34是关于 Sampling 的。

Precomputing

Data Cube

还是以之前的交易数据举例,假如我们只关注零部件(Part)、供应商(Supplier)和客户(Customer)三个维度,关注的指标是总销售额,那么我们预先可以分别把每个不同部件 p、供应商 s、客户 c 的销售额总和统计出来,以 (p, s, c) 形式存起来,如果有相关查询请求,直接返回结果就可以了。

这就是一个三维 Data Cube 的建立和使用,每一个 cell 代表一种部件、供应商、客户组合 (p, s, c),对应的 value 就是这个组合的销售额总计。

Build Data Cube

当然,实际情况下,分析任务关注的维度肯定不仅是三个,可能是多个不同的维度组合。

对 data cube 的建立有如下三种方式:

  1. 预先计算出所有组合的 data cube,之后所有的请求就可以得到最快的响应,但会带来很大的预计算和数据存储压力。(如果有 K 的维度,需要执行 个 Group by 语句来做预计算。)
  2. 不做任何预计算,每个请求都直接从原始数据进行提取,这种方式没有额外的数据存储压力,但数据量大的情况下请求执行耗时会非常长。
  3. 预先计算一些维度组合的 data cube,这个是 1 采取的方式,这种方式目标是做到请求执行耗时、预计算耗时和存储的平衡,但选择哪些维度组合做预计算是关键,选择错了,可能还不如采用上面两种方式。

前面的例子,要全部预计算出部件、供应商和客户三个维度的 data cube,需要如下 8 个组合:

  1. psc (part, supplier, customer) (6M: 6 million rows)
  2. pc (part, customer) (6M)
  3. ps (part, supplier) (0.8M)
  4. sc (supplier, customer) (6M)
  5. p (part) (0.2M)
  6. s (supplier) (0.01M)
  7. c (customer) (0.1M)
  8. none (1)

(组合后面的数字代表该组合所有结果数据的行数)

可以发现,其实如果 psc 的数据有了,pc 可以通过按 supplier 维度汇聚 psc 的数据得到,p 可以通过按 cutomer 维度汇聚 pc 的数据得到,其他依次类推。

pc 组合和 psc 组合都有 6 百万行记录,也就是对 pcpsc 两个维度组合进行查询都要遍历这么多行记录,那么如果不预计算 pc,而在用户请求 pc 维度时直接通过对 psc 维度进行汇聚,遍历的数据行数是一致的,如果以数据行数作为衡量指标,预计算 pc 便是毫无必要的。

The Lattice Framework

1 中提出了一个 Lattice 模型,如下图所示:

每个节点表示一个 data cube 组合,下方的节点可以通过上方节点汇聚得到。

左边是上面例子的 lattice 模型,右边是模型之间的合并过程。

通过这样的结构,可以将维度组合选择转化为一个最优选择的问题:

在限制节点个数的情况下,最小化每个节点预计算的平均耗时。

1 中首先提出了个 cost model 来评估通过依赖节点计算自身 data cube 的 cost,然后提出了个 greedy algorithm 通过计算平均最少 cost 来进行预计算维度选择,算法的细节和证明大家可以细读该论文

2 中给出了一种基于内存的 data cube 计算方法,有兴趣可以下载阅读

Sampling

Data Cube 模式,不论如何优化,都是需要离线任务去预先构建大量的 Cube 集,在需要的维度很多、或者数据延迟要求很低的场景下,不能很好的满足要求。

Sampling 方式是在降低准确性的前提下,减少遍历的数据量,达到快速响应查询请求的目的。

4 中通过对用户的查询请求进行统计,评估出经常用的查询列集合,预先进行 Sample 创建。

Sample Creation

如何进行 Sample 创建,我在看论文的时候,直接想到的是将数据记录打乱,随机分布在若干个 partition 里面,当有请求过来的时候,直接选择一个或多个 partition 进行查询即可。

4 中提到了这样做(uniform sampling)的问题:

如果只是全局的对数据做统计,效果比较好,但如果有 filter 或者 group by 操作,这种方式往往得不到好的效果。

举个例子,比如我要按城市来统计销售额的分布,如果是采用我想的那种分布方式的话,一些交易量很少的城市,可能在 sample 里完全消失了,这样的分布统计,其实是错的。

4 中提出了 Stratified Sampling 方式来解决这个问题。

基本思想就是首先对维度列进行统计,将相同列值的行作为一个 group,然后分别进行 sampling,论文中详细介绍了 sampling 的方法和每个 group sampling size 的设定。

Sample Selection

在如何选择 Sample 的问题上,4 提出了 ELP(Error Latency Profile)模型,通过用户设定的准确率和耗时要求,进行 sample 选择。

当然,这是个非常复杂的过程。4 中详细讲了如何去评估各个 Sample 的耗时和准确率,怎么样在生成执行计划的过程中考虑用户的准确率和耗时的要求。有兴趣大家可以详细阅读

3 通过提出的如随机数据访问、在线排序、ripple join 等算法,在已有的关系型数据库,实现了一套支持 online sampling 的原型系统,有兴趣可以详细阅读

Summary

Data Cube 方案,在数据的准确度方面是毋须质疑优于 Sampling 方案的,工程界的 Apache Kylin 就是如此的方式。而 Sampling 方式目前的应用还比较少,对于很多用户而言,Sampling 的方案即使是 99% 的准确度,还是无法接受的,哪怕其实已经满足了他的需求。

但我倒比较看好 Sampling 方式,因为 Data Cube 的整个机制对数据变化和实时方面有很大的限制,随着内存越来越廉价,以及越来越好的列存储方案,数据进行实时交互分析变得越来越可行,比如 ImpalaPresto,在大数据量的情况下,性能都很好,不大的集群都可以做到秒级响应对亿级数据量的查询。

当然,资源不可能是无限的,也不可能每个查询请求都能有资源保证快速遍历海量数据,所以,通过对准确率方面的牺牲,达到查询耗时的降低,其实是一种比较经济的方案 (Presto 是有类似 4 中提到的 Sampling 方案)。

Reference

[1] Venky Harinarayan, Anand Rajaraman, Jeffrey D. Ullman. Implementing Data Cubes Efficiently. SIGMOD, 1996.

[2] Yihong Zhao, Prasad M. Deshpande, Jeffrey F. Naughton. An Array-Based Algorithm for Simultaneous Multidimensional Aggregates. SIGMOD, 1997.

[3] Joseph M. Hellerstein, Ron Avnur, Vijayshankar Raman. Informix under CONTROL: Online Query Processing. Data Mining and Knowledge Discovery, 4(4), 2000, 281-314.

[4] Sameer Agarwal, Barzan Mozafari, Aurojit Panda, Henry Milner, Samuel Madden, Ion Stoica. BlinkDB: Queries with Bounded Errors and Bounded Response Times on Very Large Data. EuroSys, 2013.

A Bug in a Java Servlet

We have a legacy system, which is a web service, receives HTTP POST from clients, parses the data, then stores them in a file.

The function of the system is simple, and people already done functional and performance test, it’s stable. As time drifted away, the system was copy and paste to some projects by only changing the data parsing logic.

I had a similar requirement recently, then I delved into the legacy code to check if it works in order to not reinventing the wheel.

WTF

At first, I noticed below code in a HttpServlet class, it allocates more than 1M memory for each HTTP POST request.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
private static final long MAX_CONTENT_LENGTH = 1024 * 1024;
private static final int BUFFER_SIZE = 4096;

...

public void doPost(HttpServletRequest request, HttpServletResponse response)
		throws ServletException, IOException {

    ...

    int requestContentBufSize = request.getContentLength() + MAX_CONTENT_LENGTH;
    ByteBuffer requestContentBuf = ByteBuffer.allocate(requestContentBufSize);
    byte[] buffer = new byte[BUFFER_SIZE];
    requestInputStream = new DataInputStream(request.getInputStream());
    int readBytes = 0;
    int totalReadBytes = 0;
    while ((readBytes = requestInputStream.read(buffer)) > 0) {
        requestContentBuf.put(buffer);
    	totalReadBytes = totalReadBytes + readBytes;
    }
    byte[] requestContent = Arrays.copyOf(requestContentBuf.array(), totalReadBytes);

    ...
}

It’s insane, I believe the memory should be the same as each HTTP POST body size. Then I changed the code.

1
int requestContentBufSize = request.getContentLength();

Deployed the service and sent one HTTP POST request to it.

curl -d 'Hello, World' http://my.server.com:9000/log

An Exception occurred.

The BufferOverflowException

After reducing the memory allocated for ByteBuffer, it overflows.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java.nio.BufferOverflowException
	at java.nio.HeapByteBuffer.put(HeapByteBuffer.java:183)
	at java.nio.ByteBuffer.put(ByteBuffer.java:830)
	at com.myproject.servlet.LogServer.doPost(LogServer.java:99)
	at javax.servlet.http.HttpServlet.service(HttpServlet.java:643)
	at javax.servlet.http.HttpServlet.service(HttpServlet.java:723)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:290)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)
	at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:233)
	at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:191)
	at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:127)
	at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:103)
	at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:109)
	at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:293)
	at org.apache.coyote.http11.Http11Processor.process(Http11Processor.java:861)
	at org.apache.coyote.http11.Http11Protocol$Http11ConnectionHandler.process(Http11Protocol.java:606)
	at org.apache.tomcat.util.net.JIoEndpoint$Worker.run(JIoEndpoint.java:489)
	at java.lang.Thread.run(Thread.java:701)

I thought I’d better dig into how does the servlet do to make ByteBuffer get its data?

  1. It creates a small buffer occupied BUFFER_SIZE (4096) bytes.
  2. It iterates the HTTP request input stream, to put the data into the small buffer.
  3. It puts the small buffer to ByteBuffer and loop back to 1.

Well, in the last loop, the data read from the HTTP request input stream might smaller than the BUFFER_SIZE, but the servlet still puts BUFFER_SIZE bytes to ByteBuffer.

Then, to fix the ExceptionBufferOverflowException, I increased the capacity of previous ByteBuffer by BUFFER_SIZE.

1
int requestContentBufSize = request.getContentLength() + BUFFER_SIZE;

Deployed again, and

curl -d 'Hello, World' http://my.server.com:9000/log

The bug was fixed.

Did I?

The ServletInputStream

When client posts huge data, what could happen?

I created a String which is 7516 bytes, and sent to server.

curl -d 'very very long string' http://my.server.com:9000/log

Sometimes, the java.nio.BufferOverflowException occurred, and sometimes it didn’t.

What went wrong?

To find the root cause, I added some logs to trace the ByteBuffer.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int requestContentBufSize = request.getContentLength() + BUFFER_SIZE;
ByteBuffer requestContentBuf = ByteBuffer.allocate(requestContentBufSize);
byte[] buffer = new byte[BUFFER_SIZE];
requestInputStream = new DataInputStream(request.getInputStream());
int readBytes = 0;
int totalReadBytes = 0;
log.debug("1: ByteBuffer position: " + requestContentBuf.position() +
        ", buffer capacity: " + requestContentBuf.capacity() +
        ", buffer remaining: " + requestContentBuf.remaining());
while ((readBytes = requestInputStream.read(buffer)) > 0) {
	requestContentBuf.put(buffer);
	totalReadBytes = totalReadBytes + readBytes;
    log.debug("2. Bytes read: " + readBytes);
    log.debug("1: ByteBuffer position: " + requestContentBuf.position() +
            ", buffer capacity: " + requestContentBuf.capacity() +
            ", buffer remaining: " + requestContentBuf.remaining());
}

The log printed when no exception,

1
2
3
4
5
- 1: ByteBuffer position: 0, buffer capacity: 11612, buffer remaining: 11612
- 2. Bytes read: 4096
- 1: ByteBuffer position: 4096, buffer capacity: 11612, buffer remaining: 7516
- 2. Bytes read: 3420
- 1: ByteBuffer position: 8192, buffer capacity: 11612, buffer remaining: 3420

The log printed when exception occurred,

1
2
3
4
5
- 1: ByteBuffer position: 0, buffer capacity: 11612, buffer remaining: 11612
- 2. Bytes read: 1356
- 1: ByteBuffer position: 4096, buffer capacity: 11612, buffer remaining: 7516
- 2. Bytes read: 1356
- 1: ByteBuffer position: 8192, buffer capacity: 11612, buffer remaining: 3420

Now, it is easy to find out the root cause is in these lines of code.

1
2
while ((readBytes = requestInputStream.read(buffer)) > 0) {
    requestContentBuf.put(buffer);

The read method call won’t put data to the buffer fully which was specified as 4096 bytes even when the input stream still has data.

And to fix it, just specify the offset and length of the small buffer.

1
2
while ((readBytes = requestInputStream.read(buffer)) > 0) {
    requestContentBuf.put(buffer, 0, readBytes);

I had increased the capacity of the ByteBuffer by BUFFER_SIZE, this change should also be reverted.

Now, the bug is fixed, and this is network programming.

Questions

“The system works a long time, and it shouldn’t have this problem or we knew it long ago”

This is because the client seldom posts data more than 4096 bytes to server.

“I have read the Javadoc of DataInputStream, the read method will put data fully to the specified buffer”

It didn’t, please read it again.

“I have tested the read method of DataInputStream on a file, it reads fully 4096 bytes in every iteration”

This is a web service, deploy it to a server and test.

“I have tested it on my local machine as a web service, and it reads fully 4096 bytes in every iteration”

This is a web service, it should be in a network.

At Last

When a potential bug was reported, we do tests to make it happen again and find the root cause.

We do not stop listening and just look for reasons to reject it.

When we find a bug, we do help others to make it reappear to collect information.

We do not sit there and just blame on others for their mistakes.