Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

uploadBlock 返回的数据重复 #17

Open
wv-y opened this issue Aug 21, 2024 · 8 comments
Open

uploadBlock 返回的数据重复 #17

wv-y opened this issue Aug 21, 2024 · 8 comments

Comments

@wv-y
Copy link

wv-y commented Aug 21, 2024

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

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

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

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

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

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

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

@wv-y
Copy link
Author

wv-y commented Aug 21, 2024

需要服务端处理去重逻辑, 去重判断业务可以加唯一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
Copy link
Author

wv-y commented Aug 21, 2024

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

@lixiaoyu0123
Copy link
Contributor

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

@wv-y
Copy link
Author

wv-y commented Aug 22, 2024

可以看到日志内容是重复的,但是两次返回的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;
                }
            }

@lixiaoyu0123
Copy link
Contributor

可以看到日志内容是重复的,但是两次返回的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设计初衷是尽可能在生命周期活着的时候尽快报完数据。 杀进程恢复数据是兜底逻辑。

@wv-y
Copy link
Author

wv-y commented Aug 22, 2024

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

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

@lixiaoyu0123
Copy link
Contributor

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

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

对的, 差不多是这个意思

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants