You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

174 lines
13 KiB
Markdown

2 years ago
# 30 | 数据评估(上):如何实现高可用的上报组件?
无论是“高效测试”中的实时监控,还是“版本发布”中的数据校验平台,我都多次提到了数据的重要性。
对于数据评估我们的期望是“又快又准”。“快”表示数据的时效性。我们希望在1小时内甚至1分钟内就可以对数据进行评估而不需要等上1天或者几天。“准”表示数据的准确性保证数据可以反映业务的真实情况不会因为数据不准确导致做出错误的产品决策。
但是“巧妇难为无米之炊”,数据平台的准确性和时效性依赖客户端数据采集和上报的能力。那应该如何保证客户端上报组件的实时性和准确性?如何实现一个“高可用”的上报组件呢?
## 统一高可用的上报组件
可能有同学会疑惑,究竟什么是“高可用”的上报组件?我认为至少需要达到三个目标:
* **数据不会丢失**。数据不会由于应用崩溃、被系统杀死这些异常情况而导致丢失。
* **实时性高**。无论是前台进程还是后台进程,所有的数据都可以在短时间内及时上报。
* **高性能**。这里主要有卡顿和流量两个维度应用不能因为上报组件的CPU和I/O过度占用导致卡顿也不能因为设计不合理导致用户的流量消耗过多。
但是数据的完整性、实时性和性能就像天平的两端,我们无法同时把这三者都做到最好。因此我们只能在兼顾性能的同时,尽可能地保证数据不会丢失,让上报延迟更小。
在“网络优化”中我不止一次的提到网络库的统一。网络库作为一个重要的基础组件无论是应用内不同的业务还是Android和iOS多端都应该用同一个网络库。
同理,上报组件也是应用重要的基础组件,我们希望打造的是统一并且高可用的上报组件。
![](https://static001.geekbang.org/resource/image/bf/9a/bf769e6e280ebeb1f8d16cbe95fe5e9a.png)
一个数据埋点的过程,主要包括采样、存储、上报以及容灾这四个模块,下面我来依次拆解各个模块,一起看看其中的难点。
![](https://static001.geekbang.org/resource/image/19/83/1933f4cf48667805a18e07ec425ea783.png)
**1\. 采样模块**
某些客户端数据量可能会非常大,我们并不需要将它们全部都上报到后台。比如说卡顿和内存这些性能数据,我们只需要抽取小部分用户统计就可以了。
采样模块是很多同学在设计时容易忽视的,但它却是所有模块中最为复杂的一项,需要考虑下面一些策略的选择。
![](https://static001.geekbang.org/resource/image/61/70/6175884b6d62d6ad5b974baa6c723870.png)
大多数的组件采用的都是PV次数采样这样的确是最简单的。但是我们更多是在性能数据埋点上采样为了降低用户的影响面我更加倾向于使用UV采样的方式。而且为了可以让更多的用户上报我也希望每天都可以更换一批新的用户。
最终我选择的方案是“UV采样 + 用户标识随机 + 每日更换用户”的方式,但是采样还需要满足三个标准。
* **准确性**。如果配置了1%的采样比例需要保证某一时刻只有1%的用户会上报这个数据。
* **均匀性**。如果配置了1%的采样比例每天都会更换不同的1%用户来上报这个数据。
* **切换的平滑性**。用户的切换需要平滑不能在用一个时间例如12点所有用户同时切换这样会导致后台数据不连贯。
实现上面这三个标准并不容易,在微信中我们采用了下面这个算法:
```
// id用户标识例如微信号、QQ号
id_index = Hash(id) % 采样率倒数
time_index = (unix_timestamp / (24*60*60)) % 采样率倒数
上报用户 =id_index == time_index
```
每个采样持续24小时使整个切换可以很平滑不会出现所有用户同时在0点更换采样策略。有些用户在早上10点切换有些用户在11点切换会分摊到24小时中。并且从一个小时或者一天的维度来看也都可以保证采样是准确的。
不同的埋点可以设置不同的采样率,它们之间是独立的、互不影响的。当然除了采样率,在采样策略里我们还可以增加其他的控制参数,例如:
* **上报间隔**可以配置每个埋点的上报间隔例如1秒、1分钟、10分钟、60分钟等。
* **上报网络**控制某些点只允许WiFi上传。
**2\. 存储模块**
对于存储模块,我们的目标是在兼顾性能的同时,保证数据完全不会丢失。那应该如何实现呢?我们首先要思考进程和存储模式的选择。
![](https://static001.geekbang.org/resource/image/c2/56/c279b91e975100271abb804280ed2356.png)
业内最常见的上报组件是“单进程写 + 文件存储 + 内存缓存”虽然这种方式实现最为简单但是无论是跨进程的IPC调用堆积IPC调用总是很慢的还是内存缓存都可能会导致数据的丢失。
回顾一下在“I/O优化”中我列出的mmap、内存与写文件的性能对比数据。
![](https://static001.geekbang.org/resource/image/a8/8e/a898e4578fe7724cc654fa9acb3cdf8e.png)
你可以看到mmap的性能非常不错所以我们最终选择的是 **“多进程写 + mmap”的方案并且完全抛弃了内存缓存**。不过mmap的性能也并不完美它在某些时刻依然会出现异步落盘所以每个进程mmap操作需要放到单独的线程中处理。
![](https://static001.geekbang.org/resource/image/a8/87/a8dfe16bc5476e7e21b755e5a62d4c87.png)
**“多进程写 + mmap”的方案可以实现完全无锁、无IPC并且数据基本不会丢失**,看上去简直完美,但是真正实现时是不是像图中那么简单呢?肯定不会那么简单,因为我们需要考虑埋点数据的聚合以及上报数据优先级。
* **埋点数据的聚合**。为了减少上报的数据量尤其是部分性能埋点我们需要支持聚合上报。大部分组件都是使用上报时聚合的方式但是这样无法解决存储时的数据量问题。由于我们使用的是mmap可以像操作内存一样操作文件中的数据可以实现性能更优的埋点数据的聚合功能。
* **上报数据优先级**。很多上报组件埋点时都会使用一个是否重要的参数对于重要的数据采用直接落地的方式。对于我们的方案来说已经默认所有的数据都是重要的。关于上报数据的优先级我建议使用上报间隔来区分例如1分钟、10分钟或者1小时。
![](https://static001.geekbang.org/resource/image/74/88/747ab81587c7c5d0ceebe4e8763aad88.png)
对于一些敏感数据可能还需要支持加密。对于加密的数据我建议使用单独的另一个mmap文件存储。
为什么上面我说的是数据基本不会丢失而不是完全不会丢失呢因为当数据还没有mmap落盘也就是处于采样、存储内部逻辑时这个时候如果应用崩溃依然会造成数据丢失。为了减少这种情况发生我们做了两个优化。
* **精简处理逻辑**。尽量减少每个埋点的处理耗时每个埋点的数据处理时间需要压缩到0.1毫秒以内。
* **KillProcess等待**。在应用主动KillProcess之前需要调用单独的函数先等待所有队列中的埋点处理完毕。
**3\. 上报模块**
对于上报模块,我们不仅需要满足上报实时性,还需要合理地优化流量的使用,主要需要考虑的策略有:
![](https://static001.geekbang.org/resource/image/5e/66/5e995f63ce97465825cdd0cfea669466.png)
为了解决后台进程的上报实时性问题,我们采用了单进程上报的策略,我推荐使用保活能力比较强的进程作为唯一的上报进程。为了更加精细地控制上报间隔,我们采用更加复杂的班车制度模式。
后来经过仔细思考,最终的上报模块采用“多进程写 + 单进程上报”。这里有一个难点那就是如何及时的收集所有已经停止的班车会不会出现多进程同步的问题呢我们是通过Linux的文件rename的原子性以及FileObserver机制实现了一套完全无锁、高性能的文件同步模型。
![](https://static001.geekbang.org/resource/image/c6/cd/c67bb5cb84ecc577e00f13af3d6c9bcd.png)
每个进程在对应优先级的文件“停车”的时候负责把文件rename到上报数据存放的目录中。因为rename是原子操作所以不用担心两个进程会同时操作到同一个文件。而对应的上报进程只需要监听上报数据目录的变化就可以实现文件状态的同步。这样就避免了多进程同步操作同一个文件的问题整个过程也无需使用到跨进程的锁。
当然上报模块里的坑还有很多很多例如合并上报文件时应该优先选择高优先级的文件对于上报的包大小在移动网络需要设置的比WiFi小一些而不同优先级的文件需要合并组包尽量吃满带宽而且在弱网络的时候我们需要把数据包设置得更小一些先上报最高优先级的数据。
**4\. 容灾模块**
虽然我们设计得上报模块已经很强大但是如果使用者调用不合理也可能会导致严重的性能问题。我曾经遇到过某个同学在一个for循环连续埋了一百万个点还有一次是某个用户因为长期没有网络导致本地堆积了大量的数据。
一个强大的组件,它还需要具备容灾的能力,本地一般可以有下面这些策略。
![](https://static001.geekbang.org/resource/image/f3/3e/f3aaa8e7762bba6c6b3a9f88faacf83e.png)
容灾模块主要是保证即使出现开发者使用错误、组件内部异常等情况,也不会给用户的存储空间以及流量造成严重问题。
## 数据自监控
通过“多进程写 + mmap + 后台进程上报 + 班车模式”我们实现了一套完全无锁、数据基本不会丢失、无跨进程IPC调用的高性能上报组件并且通过容灾机制它还可以实现异常情况的自动恢复。
那线上效果是不是真的这么完美?我们怎样确保上报组件的数据可靠性和时效性呢?答案依然是**监控**,我们需要建立一套完善的自监控体系,为后续进一步优化提供可靠的数据支撑。
**1\. 质量监控**
上报组件的核心数据指标主要包括以下几个:
![](https://static001.geekbang.org/resource/image/9f/79/9f62e5c711224b8589bce6ec125ed279.png)
当然,如果我们追求更高的实时性,可以选择计算小时到达率,甚至是分钟到达率。
**2\. 容灾监控**
当客户端出现容灾处理时,我们也会将数据单独上报到后台监控起来。
![](https://static001.geekbang.org/resource/image/59/f9/59cc80a728e17cecd23b710c29bbfdf9.png)
除了异常情况的监控我们还希望将用户每日使用的移动流量和WiFi流量做更加细粒度的分区间监控例如01MB的占比、15MB的占比等。
## 总结
网络和数据都是非常重要的基础组件,今天我们一起打造了一款跨平台、高可用的上报组件。这也是目前比较先进的方案,在各方面的质量指标都比传统的方案有非常大的提升。
![](https://static001.geekbang.org/resource/image/02/b4/022ec87c80a928d1fa536dcde99bb3b4.png)
当然真正落实到编码这里面还有非常多的细节需要考虑也还有大大小小很多暗坑。而且虽然我们使用C++实现但是也还需要处理不同平台的些许差异比如iOS根本不需要考虑多进程问题等。
在实践中我的体会是,当我们亲自动手去实现一个网络库或者上报组件的时候,才会深深体会到把一个新东西做出来并不困难,但是如果想要做到极致,那必然需要经过精雕细琢,更需要经过长时间的迭代和优化。
## 课后作业
你所在的公司,目前正在使用哪个数据上报组件?它存在哪些问题呢?欢迎留言跟我和其他同学一起讨论。
今天的课后作业是,在实现方案中我故意隐去了两个细节点,这里把它们当作课后作业留给你,请你在留言中写下自己的答案。
**1\. 采样策略的更新**。当我们服务器采样策略更新的时候,如果不使用推送,怎样保证新的采样策略可以以最快速度在客户端生效?
**2\. 埋点进程突然崩溃**。你有没有想到如果Process A突然崩溃那哪个进程、在什么时机、以哪种方式应该负责把Process A对应的埋点数据及时rename到上报数据目录
![](https://static001.geekbang.org/resource/image/c6/cd/c67bb5cb84ecc577e00f13af3d6c9bcd.png)
欢迎你点击“请朋友读”,把今天的内容分享给好友,邀请他一起学习。最后别忘了在评论区提交今天的作业,我也为认真完成作业的同学准备了丰厚的“学习加油礼包”,期待与你一起切磋进步哦。