流式统计的几个难点

在本文发出之后不久,老外就写了一篇类似内容的。人家比我写得好,推荐大家读这篇
http://radar.oreilly.com/2015/08/the-world-beyond-batch-streaming-101....

流式统计听着挺容易的一个事情,说到底不就是数数嘛,每个告警系统里基本上都有一个简单的流式统计模块。但是当时基于storm做的时候,这几个问题还是困扰了我很长时间的。没有用过spark streaming/flink,不知道下面这些问题在spark streaming/flink里是不是都已经解决得很好了。

时间窗口切分问题

做流式统计首要的问题是把一个时间窗口内的数据统计到一起。问题是,什么是时间窗口?有两种选择

  • 日志时间(event timestamp)
  • 墙上时间(wall clock)

最简单的时间窗口统计的是基于“墙上时间”的,每过1分钟就切分出一个新窗口出来。比如statsd,它的窗口切分就是这样的。这种基于“墙上时间”的统计有一个非常严重的问题是不能回放数据流。当数据流是实时产生的时候,“墙上时间”的一分钟也就只会有一分钟的event被产生出来。但是如果统计的数据流是基于历史event的,那么一分钟可以产生消费的event数量只受限于数据处理速度。另外event在分布式采集的时候也遇到有快有慢的问题,一分钟内产生的event未必可以在一分钟内精确到达统计端,这样就会因为采集的延迟波动影响统计数据的准确性。实际上基于“墙上时间”统计需要

collection latency = wall clock - event timestamp

基于“墙上时间”的统计需要采集延迟非常小,波动也很小才可以工作良好。大部分时候更现实的选择是需要基于“日志时间”来进行窗口统计的。

使用“日志时间”就会引入数据乱序的问题,对于一个实时event stream流,其每个event的timestamp未必是严格递增的。这种乱序有两种因素引入:

  • event产生的机器的时钟不完全同步(NTP有100ms左右的不同步)
  • event从采集到到达kafka的速度不均衡(不同的网络线路有快有慢)

我们希望的流式统计是这样的:

图片描述

但是实际上数据只是基本有序的,也就是在时间窗口的边缘会有一些event需要跨到另外一个窗口去:

图片描述

最简单的分发event到时间窗口代码是这样的

window index = event timestamp / window size

对1分钟的时间窗口 window size 就是60,timestamp除以60为相同window index的event就是在同一个时间窗口的。问题的关键是,什么时候我可以确信这个时间窗口内的event都已经到齐了。如果到齐了,就可以开始统计出这个时间窗口内的指标了。然后突然又有一个落后于大伙的event落到这个已经被计算过的时间窗口如何处理?

  • 对于大部分统计而言,一个时间窗口统计出多条结果存入db并不是什么大的问题,从db里查询的时候把多条结果再合并就可以了。
  • 对于一些类型的统计(非monad),比如平均值,时间窗口内的event分为两批统计出来的结果是没有办法被再次汇总的。
  • 实时类的计算对时间敏感,来晚了的数据就没有意义了。比如告警,一个时间窗过去了就没有必要再理会这个时间窗口了。

所以对于来晚了的数据就两种策略:要么再统计一条结果出来,要么直接丢弃。要确定什么时候一个时间窗口内的event已经到齐了,有几种策略:

  • sleep 等待一段时间(墙上时间)
  • event timestamp超过了时间窗口一点点不关闭当前时间窗口,而是要等event timestamp大幅超出时间窗口的时候才关闭窗口。比如12:05:30秒的event到了才关闭12:04:00 ~ 12:05:00的时间窗口。
  • 一两个event超出了时间窗口不关闭,只有当“大量”的event超出时间窗口才关闭。比如1个event超过12:05分不关闭,如果有100个event超过了12:05的时间窗口就关闭它。

三种策略其实都是“等”,只是等的依据不同。实践中,第二种策略也就是根据“日志时间”的等待是最容易实现的。如果对于过期的event不是丢弃,而是要再次统计一条结果出来,那么过期的窗口要重新打开,又要经过一轮“等待”去判断这个过去的窗口什么时候再被关闭。

在spark上已经有人做类似的尝试了:Building Big Data Operational Intelligence platform with Apache Spark - Eric Carr (Guavus)

多流合并的问题

一个kafka的partition就是一个流,一个kafka topic的多个partition就是多个独立的流(offset彼此独立增长)。多个kafka topic显然是多个独立的流。流式统计经常需要把多个流合并统计到一起。这种里会遇到两个难题

  • 多个流的速度不一样,如何判断一个时间窗口内的event都到齐了。如果按照前面的等待策略,可能处理一个流内部的基本有序局部乱序是有效的,但是对于多个流速差异很大的流就无能为力了。一个很快的流很容易把时间窗口往后推得很远,把其他流远远跑到后面。
  • 流速不均不能靠下游兜着,下游的内存是有限的。根本上是需要一种“背压”的机制,让下游通知流速过快的上游,你慢点产生新的event,等等其他人。

举一个具体的例子:

spout 1 emit 12:05
spout 1 emit 12:06
spout 2 emit 12:04
spout 1 emit 12:07
spout 2 emit 12:05 // this is when 12:05 is ready

要想知道12:05这个时间窗的event都到齐了,首先要知道相关的流有几个(在这例子里是spout1和spout2两个流),然后要知道什么时候spout1产生了12:05的数据,什么时候spout2产生了12:05的数据,最后才可以判断出来12:05的数据是到齐了的。在某个地方要存一份这样的流速的数据去跟踪,在窗口内数据到齐之后发出信号让相关的下游往前推动时间窗口。考虑到一个分布式的系统,这个跟踪要放在哪个地方做,怎么去通知所有的相关方。

极端一些的例子

spout 1 emit 13:05
spout 2 emit 12:31
spout 1 emit 13:06
spout 2 emit 12:32

多个流的流速可能会相差到半个小时以上。考虑到如果用历史的数据汇入到实时统计系统里时,很容易因为计算速度不同导致不同节点之间的处理进度不一致。要计算出正确的结果,下游需要缓存这些差异的半个小时内的所有数据,这样很容易爆内存。但是上游如何感知到下游要处理不过来了呢?多个上游之间又如何感知彼此之间的速度差异呢?又有谁来仲裁谁应该流慢一些呢?

一个相对简单的做法是在整个流式统计的分布式系统里引入一个coordinator的角色。它负责跟踪不同流的流速,在时间窗口的数据到齐之后通知下游flush,在一些上游流速过快的时候(比如最快的流相比最慢的流差距大于10分钟)由coordinator发送backoff指令给流速过快的上游,然后接到指令之后sleep一段时间。一段基本堪用的跟踪不同流流速的代码:https://gist.github.com/taowen/2d0b3bcc0a4bfaecd404

数据一致性问题

低档一些的说法是这样的。假设统计出来的曲线是这样的:

图片描述

如果中间,比如08:35左右重启了统计程序,那么曲线能否还是连续的?

高档一些的说法是,可以把流式统计理解为主数据库与分析数据库之间通过kafka消息队列进行异步同步。主数据库与分析数据库之间应该保持eventual consistency。

图片描述

要保证数据不重不丢,就要做到生产到kafka的时候,在主数据库和kafka消息队列之间保持一个事务一致性。举一个简单的例子:

用户下了一个订单
主数据库里插入了一条订单的数据记录
kafka消息队列里多了一条OrderPlaced的event

这个流程中一个问题就是,主数据插入成功了之后,可能往kafka消息队列里enqueue event失败。如果把这个操作反过来

用户下了一个订单
kafka消息队列里多了一条OrderPlaced的event
主数据库里插入了一条订单的数据记录

又可能出现kafka消息队列里enqueue了,但是主数据库插入失败的情况。就kafka队列的目前的设计而言,对这个问题是无解的。一旦enqueue的event,除非过期是无法删除的。

在消费端,当我们从kafka里取出数据之后,去更新分析数据库的过程也要保持一个分布式事务的一致性。

取出下一条OrderPlaced evnet(指向的offset+1)
当前时间窗的统计值+1
重复以上过程,直到窗口被关闭,数据写入到分析数据库

kafka的数据是可以重放的,只要指定offset就可以把这个offset以及之后的数据读取出来。所谓消费的过程就是把客户端保存的offset值加1的过程。问题是,这个offset指针保存在哪里的问题。常规的做法是把消费的offset保存到zookeeper里。那么这就有一个分布式的一致性问题了,zookeeper里offset+1了,但是分析数据库并没有实际把值统计进去。考虑到统计一般不是每条输入的event都会更新分析数据库,而是把中间状态缓存在内存中的。那么就有可能消费了成千上万个event,状态都在内存里,然后“啪”的一下机器掉电了。如果每次读取event都移动offset的话,这些event就丢掉了。如果不是每次都移动offset的话,又可能在重启的时候导致重复统计。

搞统计的人在乎这么一两条数据吗?其实大部分人是不在乎的。不少团队压根连offset都不保存,每次开始统计直接seek到队列的尾部开始。实时计算嘛,实时最重要了。准确计算?重放历史?这个让hadoop搞定就好了。但是如果就是要较这个真呢?或者我们不追求严格的强一致,只要求重启之后曲线不断开那么难看就好了。

别的流式计算框架不清楚,storm的ack机制是毫无帮助的。

05172143-87d95ee999344b2f9add48f55edbc9d4.png

storm的ack机制是基于每个message来做的。这就要求如果做一个每分钟100万个event的统计,一分钟就要跟踪100万个message id。就算是100万个int,也是一笔相当可观的内存开销。要知道,从kafka里读出来的event都是顺序offset的,处理也是顺序,只要记录一个offset就可以跟踪整个流的消费进度了。1个int,相比100万个int,storm的per message ack的机制对于流式处理的进度跟踪来说,没有利用消息处理的有序性(storm根本上假设message之间是彼此独立处理的),而变得效率低下。

要做到强一致是很困难的,它需要把

  • 更新保存的offset
  • 更新插入分析数据库

变成一个原子事务来完成。大部分分析数据库都没有原子性事务的能力,连插入三条数据都不能保持同时变为可见,且不说还要用它来记录offset了。考虑到kafka在生产端都无法提供分布式事务,event从生产出来就不是完全一致的(多产生了或者少产生了),真正高一致的计费场景还是用其他的技术栈。所以值得解决的问题是,如何在重启之后,把之前重启的时候丢弃掉的内存状态重新恢复出来,使得统计出来的曲线仍然是连续的。

解决思路有三点:

  • 上游备份策略:重启的时候重放kafka的历史数据,恢复内存状态
  • 中间状态持久化:把统计的状态放到外部的持久的数据库里,不放内存里
  • 同时跑两份:同时有两个完全一样的统计任务,重启一个,另外一个还能正常运行。

内存状态管理的问题

做流式统计的有两种做法:

  • 依赖于外部存储管理状态:比如没收到一个event,就往redis里发incr增1
  • 纯内存统计:在内存里设置一个counter,每收到一个event就+1

基于外部存储会把整个压力全部压到数据库上。一般来说流式统计的流速是很快的,远大于普通的关系型数据库,甚至可能会超过单台redis的承载。这就使得基于纯内存的统计非常有吸引力。大部分的时候都是在更新时间窗口内的内存状态,只有当时间窗口关闭的时候才把数据刷到分析数据库里去。刷数据出去的同时记录一下当前流消费到的位置(offset)。

图片描述

这种纯内存的状态相对来说容易管理一些。计算直接是基于这个内存状态做的。如果重启丢失了,重放一段历史数据就可以重建出来。

但是内存的问题是它总是不够用的。当统计的维度组合特别多的时候,比如其中某个字段是用户的id,那么很快这个内存状态就会超过单机的内存上限。这种情况有两种办法:

  • 利用partition把输入的input分割,一个流分成多个流,每个统计程序需要跟踪的维度组合就变少了
  • 把存储移到外边去

简单地在流式统计程序里开关数据库连接是可以解决这个容量问题的:

图片描述

但是这种对外部数据库使用不小心就会导致两个问题:

  • 处理速度慢。不用一些批量的操作,数据库操作很快就会变成瓶颈
  • 数据库的状态不一直。内存的状态重启了就丢失了,外部的状态重启之后不丢失。重放数据流就可能导致数据的重复统计

但是这种把窗口统计的中间状态落地的好处也是显而易见的。重启之后不用通过重算来恢复内存状态。如果一个时间窗口有24小时,重算24小时的历史数据可能是很昂贵的操作。

版本跟踪,批量等都不应该是具体的统计逻辑的实现者的责任。理论上框架应该负责把冷热数据分离,自动把冷数据下沉到外部的存储,以把本地内存空闲出来。同时每次小批量处理event的时候都要记录处理的offset,而不是要等到窗口关闭等待时候。

图片描述

数据库状态和内存状态要变成一个紧密结合的整体。可以把两者的关系想象成操作系统的filesystem page cache。用mmap把状态映射到内存里,由框架负责什么时候把内存里的变更持久化到外部存储里。

总结

基于storm做流式统计缺乏对以下四个基本问题的成熟解决方案。其trident框架可能可以提供一些答案,但是实践中好像使用的人并不多,资料也太少了。可以比较自信的说,不仅仅是storm,对于大多数流式计算平台都是如此。

  • 时间窗口切分的问题
  • 多流合并的问题
  • 数据一致性问题(重启之后曲线断开的问题)
  • 内存状态管理问题

这些问题要好好解决,还是需要一番功夫的。新一代的流式计算框架比如spark streaming/flink应该有很多改进。即便底层框架提供了支持,从这四个角度去考察一下它们是如何支持的也是非常有裨益的事情。

<think>好的,用户想做一个可以在苹果手机上运行的日语能力考试软件。首先,我需要确定用户的需求是什么。日语能力考试分为N1到N5五个级别,所以软件需要涵盖这些级别的所有内容,包括词汇、语法、阅读和听力。用户可能希望这个软件有模拟考试功能、题库练习、进度跟踪以及错题本之类的功能。 接下来,技术选型方面,苹果手机的话自然是用Swift语言开发,iOS原生应用体验更好。可能需要用到Xcode作为开发工具,数据库的话可以考虑Core Data或者SQLite,或者云数据库如Firebase,方便同步用户数据。音频播放对于听力部分很重要,要确保能流畅播放并支持后台播放。 然后,功能模块需要详细拆分。用户模块包括注册登录和个人信息管理;题库管理需要覆盖所有级别的题目,并且支持题型多样性;模拟考试要定时和评分;听力部分需要处理音频文件的存储和播放;学习统计要展示进度和弱点;还有错题收集和复习功能。推送通知提醒学习可能也是用户需要的。 开发步骤方面,先做市场调研,了解竞品情况,确定自己的独特卖点。然后设计UI/UX,流程要符合用户习惯。开发阶段分模块进行,比如先做用户系统,再开发题库,接着听力模块,最后整合模拟考试。测试环节非常重要,尤其是不同设备上的兼容性测试,防止崩溃或UI错乱。上线后还要持续收集反馈,迭代更新。 合规性方面需要注意用户隐私,符合苹果的App Store审核规则,特别是数据安全和支付方面的规定。如果涉及到应用内购买,需要配置好支付系统。另外,日语考试的题目可能涉及版权问题,要确保题目来源合法,或者自己原创题目。 技术难点可能包括听力同步显示题目,实时保存考试进度,以及智能推荐算法根据用户表现调整题目难度。需要优化音频流式传输,减少延迟。数据同步方面,如果支持多设备,要处理好冲突和合并。 成本估算方面,开发团队需要iOS开发、后端、UI/UX设计师、测试人员,可能还有内容制作团队编写题目。时间大概6-12个月,费用从几十万到上百万不等,取决于团队规模和功能复杂度。 最后,运营策略要考虑如何推广,比如通过日语学习社区、社交媒体合作,或者与教育机构合作。用户反馈机制也很重要,及时修复问题并添加新功能,保持用户活跃度。 可能还需要考虑离线使用,因为用户可能在没有网络的情况下使用,特别是听力部分的大文件需要提前下载。同时,适配不同的iOS版本和设备型号,确保兼容性。另外,辅助功能比如字体大小调整、夜间模式等也能提升用户体验。</think>开发日语能力考试(JLPT)备考类iOS应用需要从技术实现、内容架构、用户体验三个维度系统规划,以下为分步骤实施方案: 一、需求分析与定位 1. 明确目标用户:定位N5-N1全级别备考需求,分析不同级别用户的学习痛点 2. 竞品调研:研究现有应用(如MojiTest、JLPT Practice)的功能结构及用户评价 3. 核心功能确认: - 自适应题库系统(需覆盖10,000+真题及模拟题) - 智能错题本(支持按语法点/题型分类) - 模考系统(完全模拟真实考试流程) - 语音训练引擎(针对听力题型优化) 二、技术架构设计 1. 开发环境: $$ Xcode 15 + SwiftUI 4.0 + Combine $$ 2. 数据架构: ```swift struct Question { var id: String var level: JLPTLevel var type: QuestionType // 文字/词汇/语法/阅读/听力 var audioURL: URL? // 听力题专用 var options: [String] var correctAnswer: Int } ``` 3. 关键技术点: - 离线音频缓存(AVFoundation框架优化) - 实时进度同步(CoreData + CloudKit) - 自适应算法(基于IRT项目反应理论) 三、核心功能实现 1. 智能题库系统: - 实现题目元数据分类存储 - 开发动态难度调节算法 ```swift func calculateDifficulty(userLevel: Double) -> [Question] { let threshold = userLevel * 0.7 return questions.filter { $0.difficulty >= threshold } } ``` 2. 听力训练模块: - 音频分段标记技术(支持精听复读) - 背景播放权限配置 ```xml <key>UIBackgroundModes</key> <array> <string>audio</string> </array> ``` 3. 考试模拟系统: - 严格计时器实现 - 自动保存机制 ```swift Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in remainingTime -= 1 if remainingTime <= 0 { autoSubmit() } } ``` 四、内容体系建设 1. 语料库构建标准: - 词汇:覆盖《新完全マスター》系列所有词条 - 语法:整合TRY!系列及新完全マスター语法体系 - 真题:获取日本国际交流基金官方授权 2. 题目数据结构示例: $$ \text{题目} = \begin{cases} \text{ID} & \text{唯一标识符} \\ \text{题干} & \text{日文原文+注音} \\ \text{选项} & [A-D] \text{或} [1-4] \\ \text{解析} & \text{中日双语解释} \end{cases} $$ 五、用户体验优化 1. 无障碍设计: - 支持动态字体大小调整 - 听力题文字同步高亮 2. 学习数据分析: - 生成能力雷达图 $$ \text{能力评估} = \frac{\sum_{i=1}^{n}w_i \cdot s_i}{\sum_{i=1}^{n}w_i} \times 100\% $$ 3. 交互细节: - 手写假名识别功能 - 语法点即时词典 六、部署与运营 1. TestFlight阶段: - 邀请200+真实用户参与beta测试 - 收集崩溃日志(使用Firebase Crashlytics) 2. App Store上线: - 准备多语言元数据(中日英) - 配置应用内购买项目(IAP) 3. 持续更新机制: - 每月新增100+题目 - 根据考试大纲动态调整 开发建议: 1. 优先申请日本国际交流基金的官方合作 2. 采用模块化开发策略(将题库/考试/错题本作为独立组件) 3. 注重本地化处理(包括日本方言听力题的特殊处理) 4. 加入AR功能辅助场景学习(需ARKit集成) 成本估算参考: - 基础版开发:6-8个月,¥500,000-800,000 - 内容授权费用:¥200,000/年 - 服务器运维:¥20,000/月 注意需遵守《苹果人机交互指南》第4.3条教育类应用规范,避免因内容版权问题导致审核被拒。建议采用免费+订阅模式,提供基础功能免费、高级解析付费的商业模式。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值