DataReporter icon indicating copy to clipboard operation
DataReporter copied to clipboard

uploadBlock 返回的数据重复

Open wv-y opened this issue 1 year ago • 8 comments

测试了下,如果app存活期间产生的日志没有全部upload,下次启动app后会返回之前已经上报过的日志; 这边也调用了UploadSucess方法;

下面截图里,点击按钮push了200条日志,(日志内容是数字0、1、3...),第一次上报到29,保存上报记录到本地,然后关闭app再启动又从0开始上报

image 大佬能帮忙看看吗?需要demo的话我能提供iOS的项目

wv-y avatar Aug 21 '24 10:08 wv-y

测试了下,如果app存活期间产生的日志没有全部upload,下次启动app后会返回之前已经上报过的日志; 这边也调用了UploadSucess方法;

下面截图里,点击按钮push了200条日志,(日志内容是数字0、1、3...),第一次上报到29,保存上报记录到本地,然后关闭app再启动又从0开始上报

image 大佬能帮忙看看吗?需要demo的话我能提供iOS的项目

需要服务端处理去重逻辑, 去重判断业务可以加唯一id来区分。 因为网络传输是不稳定的,端上是在服务端确认收到数据后才删除掉本地数据。 但是如果在这个传输过程中出现网络问题,或者端上杀进程。 为了保证数据不丢失, 端上都不会删除数据,端上会在服务端明确收到数据后,才删除本地数据,否则,下一次启动会上报上一次没有确认收到的数据。 所以解决这个重复的问题方案就需要 业务的服务端进行数据去重处理。

lixiaoyu0123 avatar Aug 21 '24 11:08 lixiaoyu0123

需要服务端处理去重逻辑, 去重判断业务可以加唯一id来区分。 因为网络传输是不稳定的,端上是在服务端确认收到数据后才删除掉本地数据。 但是如果在这个传输过程中出现网络问题,或者端上杀进程。 为了保证数据不丢失, 端上都不会删除数据,端上会在服务端明确收到数据后,才删除本地数据,否则,下一次启动会上报上一次没有确认收到的数据。 所以解决这个重复的问题方案就需要 业务的服务端进行数据去重处理。

服务端去重逻辑确实是有的; 这个库也集成到项目里好久了,但是线上发现有用户上报的日志还是半年之前的,占比能达到10%~20%,这个比例不正常;同时logid重复的情况很多,生成ID的api,iOS这边使用的[[NSUUID UUID] UUIDString]; 几乎不会重复的。 我这边用demo测试了下,怀疑有调用uploadSuccess时没有成功删除的情况;

以下是测试逻辑: 1、push200条日志; 2、上传日志时,记录下来上传记录(key和日志内容); 3、上传一部分后将上传记录写入本地,并杀死app; 4、然后再启动app可以看到又从0开始上传

如果等这200条日志全部上传完成再杀死app后重启则不会有重复上报现象。

大佬能帮忙分析下吗?

下面是部分逻辑代码

MyViewController

//
//  DataReporterManager.m
//  reporterPath
//
//  Created by luojilab on 2018/11/10.
//  Copyright © 2018年 luojilab. All rights reserved.
//

#import "DataReporterManager.h"
#import <DataReporter/DataReporter.h>

static NSString *kReporterIndentify = @"kReporterIndentify";
static NSUInteger kMaxFileSize = 100 * 1024;

//static instance
static void *reporterInstanse;

@interface DataReporterManager()

@property (nonatomic,copy) NSString *reporterPath;

@end

@implementation DataReporterManager

#pragma mark - life cycle

- (void)dealloc{
    [DataReporterManager stopMonitorReport];
}

+ (instancetype)sharedInstance {
    static id singleton = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        singleton = [[super allocWithZone:nil] initInstance];
    });
    return singleton;
}

- (instancetype)initInstance {
    self = [super init];
    if (self) {
      
    }
    return self;
}


#pragma mark - private Methods


/**
 初始化注册登录通知
 */
- (void)addNotification {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
      
    });
}

/**
上报数据到服务器
*/
- (void)uploadData:(int64_t)key dataArrays:(NSArray *)dataArrays {
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        [self uploadSuccessed:key]; // 默认成功
       // 返回数据记录
        !self.uploadDataCallback ?: self.uploadDataCallback(key, dataArrays);
    });
}

- (void)uploadSuccessed:(int64_t)key {
    //重要,通知DataReporter,report success,每次回调必须执行成功或者失败
    dispatch_async(dispatch_get_main_queue(), ^{
        [DataReporter UploadSucess:reporterInstanse key:key];
        DebugLog(@"UploadSucess -> should not upload again = %lld",key);
    });
}

- (void)uploadFailed:(int64_t)key {
    //重要,通知DataReporter,report Failed,每次回调必须执行成功或者失败
    //失败后,间隔一段时间会重新发送,上层不必多余处理
    dispatch_async(dispatch_get_main_queue(), ^{
        [DataReporter UploadFailed:reporterInstanse key:key];
        DebugLog(@"UploadFailed -> should upload again = %lld",key);
    });
}

/**
 初始化实例
 */
- (void)initReporterInstance{
    
    [DataReporterManager stopMonitorReport];

    reporterInstanse = [DataReporter MakeReporter:kReporterIndentify cachePath:self.reporterPath encryptKey:@"" uploadBlock:^(int64_t key, NSArray *dataArrays) {
        if (dataArrays== nil || [dataArrays count] == 0 || reporterInstanse == nil){
            return;
        }
       
        dispatch_async(dispatch_get_main_queue(), ^{
            [self uploadData:key dataArrays:dataArrays];
        });
    }];
    //set report max count  设置每次上报最大的数据量 10表示,一次最多10条报一次
    [DataReporter SetReportCount:reporterInstanse count:10];
	
	//set report ExpiredTime 0表示永久有效 所有数据上报,10*24*60*60 表示10天内有效,10天外数据不上报
    [DataReporter SetExpiredTime:reporterInstanse expiredTime:0];
	
	//set report reporterInstanse 上报间隔 单位i毫秒  10 表示每隔10毫秒上报一次,0表示有数据立即上报
    [DataReporter SetReportingInterval:reporterInstanse reportingInterval:1000*10];
	
	//set report retryInterval 重试间隔 单位i秒  5 表示每第一次上报错误后延迟5秒重试上报,再次错误,再加5秒,也就是10秒后再重试,最长1个小时后重试,如果为0,表示出错后立即重试,容易导致服务器压力过大,默认值为5
    [DataReporter SetRetryInterval:reporterInstanse retryInterval:5];
	
    //set save file size 设置缓存文件大小, 大小一定要比单条push进来的数据大
    [DataReporter SetFileMaxSize:reporterInstanse fileMaxSize:kMaxFileSize];
}
                               

/**
 开始上报任务
 */
- (void)startReporter{
    
    if (reporterInstanse) {
        [DataReporter Start:reporterInstanse];
    }
    
}

#pragma mark - public Methods


/**
 start Report
 */
+ (void)startMonitorReport{
    [[self sharedInstance] initReporterInstance];
    [[self sharedInstance] startReporter];
}


/**
 save ReportData
 
 @param data reportData
 */
+ (void)pushData:(NSData *)data{

    if ([data length] == 0){
        return;
    }
    if (!reporterInstanse) {
        return;
    }
    [DataReporter Push:reporterInstanse byteArray:data];
}


/**
 Stop - 结束上报
 */
+ (void)stopMonitorReport{
    if (reporterInstanse == nil) {
        return;
    }
    [DataReporter ReleaseReporter:reporterInstanse];
    reporterInstanse = NULL;

}
@end

MyViewController


#import "MyViewController.h"
#import "DataReporterManager.h"

static NSString *log_key_UserDefaults = @"logData";

@interface MyViewController ()

@property (nonatomic, strong) UIButton *saveButton;
@property (nonatomic, strong) UIButton *pushButton;
@property (nonatomic, strong) UITextView *textView;
@property (nonatomic, strong) NSString *logInfo;

@end

@implementation MyViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.view.backgroundColor = [UIColor whiteColor];
    
    self.pushButton = [UIButton buttonWithType:UIButtonTypeSystem];
    [self.pushButton setTitle:@"pushData" forState:UIControlStateNormal];
    [self.pushButton setFrame:CGRectMake(30, 100, 80, 30)];
    [self.pushButton setBackgroundColor:[UIColor lightGrayColor]];
    [self.pushButton addTarget:self action:@selector(pushData) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:self.pushButton];
    
    self.saveButton = [UIButton buttonWithType:UIButtonTypeSystem];
    [self.saveButton setTitle:@"saveData" forState:UIControlStateNormal];
    [self.saveButton setFrame:CGRectMake(150, 100, 80, 30)];
    [self.saveButton setBackgroundColor:[UIColor lightGrayColor]];
    [self.saveButton addTarget:self action:@selector(saveData) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:self.saveButton];
    
    self.textView = [[UITextView alloc] init];
    self.textView.font = [UIFont systemFontOfSize:15];
    self.textView.scrollEnabled = YES;
    self.textView.editable = NO;
    self.textView.layoutManager.allowsNonContiguousLayout = NO;
    self.textView.translatesAutoresizingMaskIntoConstraints = NO;
    [self.view addSubview:self.textView];
    
    [self.view addConstraints:@[leftConstraint, rightConstraint, topConstraint, bottomConstraint]];

    [[DataReporterManager sharedInstance] setUploadDataCallback:^(int64_t key, NSArray *array) {
        [self showLogWithKey:key data:array];
    }];
    
    self.logInfo = [[NSUserDefaults standardUserDefaults] objectForKey:log_key_UserDefaults];
    self.textView.text = self.logInfo;
}

- (void)pushData {
    //big data save and reporter
    for (NSUInteger i = 0; i < 200; i++) {
        NSString *str = [NSString stringWithFormat:@"%ld",(long)i];
        NSData *data = [NSData dataWithBytes:[str UTF8String] length:[str length]];
        [DataReporterManager pushData:data];
    }
}

- (void)showLogWithKey:(int64_t)key data:(NSArray *)array {
    dispatch_async(dispatch_get_main_queue(), ^(void) {
        NSString *temp = @"";
        for (NSData *data in array) {
            NSString *log = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
            NSString *str = [NSString stringWithFormat:@"key:%lld,log:%@\n", key, log];
            NSLog(@"%@", str);
            temp = [NSString stringWithFormat:@"%@%@", temp, str];
        }
        self.logInfo = [NSString stringWithFormat:@"%@%@", self.logInfo?:@"", temp];
        
        self.textView.text = self.logInfo;
    });
}

- (void)saveData {
    NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
    [defaults setObject:self.logInfo forKey:log_key_UserDefaults];
}

@end

wv-y avatar Aug 21 '24 11:08 wv-y

可以看到日志内容是重复的,但是两次返回的int64_t key不同,这个key的生成逻辑是什么样的呀?

wv-y avatar Aug 21 '24 12:08 wv-y

可以看到日志内容是重复的,但是两次返回的int64_t key不同,这个key的生成逻辑是什么样的呀? int64_t key 是根据时间算的,重启后第二次肯定不一样。 SetFileMaxSize 中 文件的大小设置适当的大小,根据单条数据的大小,设置一个合理的大小,这样当文件达到一定条数就会落盘。一旦上传成功就会清理单片落盘文件。就不会出现上面这种数据很多重复上报情况。 DataReporter是一个准实时的上报设计, 适用于实时上报比较频繁场景。 如果数据量巨大。 实时场景不强的,像Log的上报可以用DataTransHub,这种上报更合适。

lixiaoyu0123 avatar Aug 22 '24 06:08 lixiaoyu0123

可以看到日志内容是重复的,但是两次返回的int64_t key不同,这个key的生成逻辑是什么样的呀? int64_t key 是根据时间算的,重启后第二次肯定不一样。 SetFileMaxSize 中 文件的大小设置适当的大小,根据单条数据的大小,设置一个合理的大小,这样当文件达到一定条数就会落盘。一旦上传成功就会清理单片落盘文件。就不会出现上面这种数据很多重复上报情况。 DataReporter是一个准实时的上报设计, 适用于实时上报比较频繁场景。 如果数据量巨大。 实时场景不强的,像Log的上报可以用DataTransHub,这种上报更合适。

感谢回复,我看了下源码,key的逻辑明白了;

另外看读数据和删除数据的方法, 删除数据时(DataProvider::ClearItem),会从内存或文件删除,这里会判断item.fromPath是否存在,ClearFile是删除整个文件,然后我看了FileInputStream::ReadData方法,只有最后一条数据会被赋值fromPath。

所以,假如某个文件有 100 条记录,每次上传 10 条,当上传到20条时进程被关闭,重启后会重新从第 1 条开始上报吗?

void DataProvider::ClearItem(CacheItem &item) {
        if (!item.fromPath.empty()) {
            ClearFile(item.fromPath);
        }
        if (item.fromMem != NULL) {
            ClearMem();
        }
    }
if (m_Offset >= fileSize) {
                if (DataProvider::IsExpired(dateInItem, expiredTime)) {
                    if (!ret->empty()) {
                        ret->back()->fromPath = m_Path;
                    }
                    i++;
                    continue;
                } else {
                    cacheItem->fromPath = m_Path;
                }
            }

wv-y avatar Aug 22 '24 09:08 wv-y

可以看到日志内容是重复的,但是两次返回的int64_t key不同,这个key的生成逻辑是什么样的呀? int64_t key 是根据时间算的,重启后第二次肯定不一样。 SetFileMaxSize 中 文件的大小设置适当的大小,根据单条数据的大小,设置一个合理的大小,这样当文件达到一定条数就会落盘。一旦上传成功就会清理单片落盘文件。就不会出现上面这种数据很多重复上报情况。 DataReporter是一个准实时的上报设计, 适用于实时上报比较频繁场景。 如果数据量巨大。 实时场景不强的,像Log的上报可以用DataTransHub,这种上报更合适。

感谢回复,我看了下源码,key的逻辑明白了;

另外看读数据和删除数据的方法, 删除数据时(DataProvider::ClearItem),会从内存或文件删除,这里会判断item.fromPath是否存在,ClearFile是删除整个文件,然后我看了FileInputStream::ReadData方法,只有最后一条数据会被赋值fromPath。

所以,假如某个文件有 100 条记录,每次上传 10 条,当上传到20条时进程被关闭,重启后会重新从第 1 条开始上报吗?

void DataProvider::ClearItem(CacheItem &item) {
        if (!item.fromPath.empty()) {
            ClearFile(item.fromPath);
        }
        if (item.fromMem != NULL) {
            ClearMem();
        }
    }
if (m_Offset >= fileSize) {
                if (DataProvider::IsExpired(dateInItem, expiredTime)) {
                    if (!ret->empty()) {
                        ret->back()->fromPath = m_Path;
                    }
                    i++;
                    continue;
                } else {
                    cacheItem->fromPath = m_Path;
                }
            }

对的,理解的完全正确。 所以可以通过预判业务单条数据大小 合理设置maxFileSize,可以尽早落盘,来达到符合自己业务场景的情况。 DataReporter设计初衷是尽可能在生命周期活着的时候尽快报完数据。 杀进程恢复数据是兜底逻辑。

lixiaoyu0123 avatar Aug 22 '24 09:08 lixiaoyu0123

对的,理解的完全正确。 所以可以通过预判业务单条数据大小 合理设置maxFileSize,可以尽早落盘,来达到符合自己业务场景的情况。 DataReporter设计初衷是尽可能在生命周期活着的时候尽快报完数据。 杀进程恢复数据是兜底逻辑。

感谢!明白了, 假设一条数据大小是2k,一次上报10条,设置maxFileSize为20-50k是不是算是比较合理呢?

wv-y avatar Aug 22 '24 10:08 wv-y

对的,理解的完全正确。 所以可以通过预判业务单条数据大小 合理设置maxFileSize,可以尽早落盘,来达到符合自己业务场景的情况。 DataReporter设计初衷是尽可能在生命周期活着的时候尽快报完数据。 杀进程恢复数据是兜底逻辑。

感谢!明白了, 假设一条数据大小是2k,一次上报10条,设置maxFileSize为20-50k是不是算是比较合理呢?

对的, 差不多是这个意思

lixiaoyu0123 avatar Aug 23 '24 03:08 lixiaoyu0123