|
1 |
| -# test 1 |
2 |
| - |
3 |
| -File: base14009.obb to base14012.obb |
4 |
| -Device: DELL PC, Intel i3 550 (4core 3.2GHz) 4GB RAM |
5 |
| -Build: MinGW 32bit release |
6 |
| - |
7 |
| -| block size | hash bloom | hatch speed | match speed | hatch blocks | match blocks | delta size | |
8 |
| -| ---------- | :--------- | :---------: | :---------: | :----------: | :----------: | ---------: | |
9 |
| -| 2048 | 0 | 2189 ms | 14620 ms | 46328 | 40418 | 11820 KB | |
10 |
| -| 4096 | 0 | 2158 ms | 9675 ms | 23164 | 18960 | 16816 KB | |
11 |
| -| 8192 | 0 | 2137 ms | 8580 ms | 11582 | 9249 | 18664 KB | |
12 |
| -| 2048 | 20 | 2182 ms | 3753 ms | 46328 | 40418 | 11820 KB | |
13 |
| -| 4096 | 20 | 2156 ms | 2914 ms | 23164 | 18960 | 16816 KB | |
14 |
| -| 8192 | 20 | 2154 ms | 2719 ms | 11582 | 9249 | 18664 KB | |
| 1 | +crsync-基于rsync rolling算法的文件增量更新 |
| 2 | +-------- |
| 3 | + |
| 4 | +**最终实现效果:** |
| 5 | +1. 无版本概念,任何本地文件均可增量升级到最新.服务器不用管理多版本 |
| 6 | +2. 内存小,100M文件升级时只占用500KB内存. |
| 7 | + |
| 8 | +**使用流程:** |
| 9 | +1. 制作新版本,上传HTTP File Server. |
| 10 | +2. Client自动计算差异,下载差异,合并差异. |
| 11 | +3. done! |
| 12 | + |
| 13 | +#0.缘起 |
| 14 | +目前主流的文件增量更新方案是**bs diff/patch**,以及google chrome Courgette改进方案。 |
| 15 | +无尽之剑Android版最初使用该方案,在封测期间发现存在问题: |
| 16 | + |
| 17 | +1. 多版本运营繁琐。 |
| 18 | +假设目前先后开发了5个版本,1.0~5.0,运营中需要制作 |
| 19 | +1-2.patch |
| 20 | +2-3.patch |
| 21 | +3-4.patch |
| 22 | +4-5.patch |
| 23 | +**n个版本,n-1个patch包** |
| 24 | +或者 |
| 25 | +1-2.patch |
| 26 | +1-3.patch 2-3.patch |
| 27 | +1-4.patch 2-4.patch 3-4.patch |
| 28 | +1-5.patch 2-5.patch 3-5.patch 4-5.patch |
| 29 | +**n个版本,(n-1)阶乘个patch包** |
| 30 | + |
| 31 | +2. patch依赖本地版本文件完整性 |
| 32 | +如果本地文件损坏或被篡改,就无法增量升级,只能重新下载完整包. |
| 33 | + |
| 34 | +3. bs diff/patch算法性能和**内存**开销太高 |
| 35 | +> from [bsdiff官网](http://www.daemonology.net/bsdiff/) |
| 36 | +> bsdiff is quite memory-hungry. It requires max(17*n,9*n+m)+O(1) bytes of memory, where n is the size of the old file and m is the size of the new file. bspatch requires **n+m+O(1)** bytes. |
| 37 | +> bsdiff runs in O((n+m) log n) time; on a 200MHz Pentium Pro, building a binary patch for a 4MB file takes about 90 seconds. |
| 38 | +> bspatch runs in O(n+m) time; on the same machine, applying that patch takes about two seconds. |
| 39 | + |
| 40 | +bsdiff执行慢,可以,200M文件制作patch包需要运行64位版本,可以. |
| 41 | +bspatch执行慢,可以。需要占用**n+m**内存,这点无法接受. |
| 42 | +无尽之剑Android版的资源包单个最大200M,在游戏运行同时进行200M+内存开销的patch操作,非常容易OOM. |
| 43 | + |
| 44 | +**那么问题来了:有没有一种版本无关,内容无关,内存开销小的增量升级方案呢?** |
| 45 | +这就是我的**crsync**: |
| 46 | +纯C语言实现,基于rsync rolling算法, Client Side计算diff和执行patch,通过curl进行HTTP range增量下载 |
| 47 | +(同类方案有zsync,用于ubuntu package增量更新) |
| 48 | + |
| 49 | +1. 客户端计算本地版本和服务器最新版本之间的diff,然后通过http range下载差异部分. |
| 50 | + |
| 51 | +2. 内存开销 = 文件size/分块size * 40字节 + HashTable开销. |
| 52 | +假设文件size 100M,分块size 16K,那么patch操作最多只需500KB内存。 |
| 53 | + |
| 54 | +3. 版本无关, 本地文件是否改动无关,只关注本地和远端之间的内容差异. |
| 55 | + |
| 56 | +#1.rsync rolling |
| 57 | +检测2个文件的差异和重复数据,有2种策略:定长分块和变长分块. |
| 58 | +rsync算法属于前者,RDC算法属于后者,多轮rsync可以看作是对定长分块的变长优化. |
| 59 | + |
| 60 | +定长分块是将A文件按固定块大小切分,计算校验值。在B文件中遍历,计算所有相同块大小的校验值,相同即一致,不同即差异. |
| 61 | +以下假设文件size=100M, 分块size=1KB,测试数据基于公司开发机i7-3770 CPU. |
| 62 | + |
| 63 | +A文件切分,计算[0]~[1023], [1024]~[2047],[2048]~[3071]...的校验值,输出一张A表. |
| 64 | +B文件遍历,计算[0]~[1023], [1]~[1024],[2]~[1025],...的校验值,在A表中遍历比对.相同即重复,不同即差异. |
| 65 | + |
| 66 | +不足1KB的数据,rsync补齐0再计算校验值,与B文件遍历计算比对. |
| 67 | +crsync不计算,直接将之存入A表末尾,当作差异. |
| 68 | + |
| 69 | +校验算法很多,常用MD4,MD5,SHA-1,BLAKE2均可. |
| 70 | +这样明显很慢,如何优化? |
| 71 | + |
| 72 | +rsync使用2个校验值,**弱校验**和**强校验**. |
| 73 | +弱校验是计算一个32位的CRC值,优点: 快; 缺点: 冲突率高;校验值相同时数据可能不同. |
| 74 | +强校验是上面说到的各种算法,优点: 冲突率低; 缺点: 慢. |
| 75 | +首先做弱校验,值相同再做强校验.以此保证快速与准确. |
| 76 | +遍历文件[0]~[1023], [1]~[1024],[2]~[1025],...需计算104856577次弱校验 |
| 77 | +这样依然很慢,如何优化? |
| 78 | + |
| 79 | +CRC校验算法很多,rsync使用**adler32**算法,因此有了**rolling**的特性. |
| 80 | +alder32算法由Mark Adler于1995发明,用于zlib. |
| 81 | +一个32位的adler32值由2个16位的A和B值构成。 |
| 82 | +A值=buffer中所有byte相加,对2^16取模(65536). |
| 83 | +b值=buffer中所有A相加,对2^16取模(65536). |
| 84 | +公式如下: |
| 85 | +> A = 1 + D1 + D2 + ... + Dn (mod 65521) |
| 86 | +> B = (1 + D1) + (1 + D1 + D2) + ... + (1 + D1 + D2 + ... + Dn) (mod 65521) |
| 87 | +> = n×D1 + (n−1)×D2 + (n−2)×D3 + ... + Dn + n (mod 65521) |
| 88 | +> Adler-32(D) = B × 65536 + A |
| 89 | + |
| 90 | +65521是65536范围内的最大素数,因此mod=65521即可. |
| 91 | +librsync(rdiff)在A和B值计算时再偏移31,rsync偏移0,crsync偏移0. |
| 92 | + |
| 93 | +有趣的是,这个公式等价于: |
| 94 | +> A += data[i]; |
| 95 | +> B += A; |
| 96 | + |
| 97 | +rolling是什么? |
| 98 | +计算出[0]~[1023]的校验值,再计算[1]~[1024]的校验值时,只需减去data[0],加上data[1024]即可!不需重新计算! |
| 99 | +> A -= data[0]; |
| 100 | +> A += data[1024]; |
| 101 | +> B -= 1024 * data[0]; |
| 102 | +> B += A; |
| 103 | + |
| 104 | +如此滑动遍历,即可得到整个文件的所有分块的校验值.(大赞!) |
| 105 | +实测文件size 100M,分块size 2K,遍历计算仅耗时275ms. |
| 106 | + |
| 107 | +#2.rsync HashTable |
| 108 | +**rolling计算足够快了,用B文件每个块的校验值check A文件的校验表,如何优化?** |
| 109 | + |
| 110 | +A文件的校验表size = 102400,冒泡?快速?折半? |
| 111 | +rsync使用二段HashTable来存储校验值,即H(A, H(B, MD5)) |
| 112 | +用adler32的A做key, B+MD5做value, 其中再用alder32的B做key, MD5做value. |
| 113 | +表大小2^16,满足A的值范围.冲突值用LinkList存储. |
| 114 | + |
| 115 | +首先命中A,再命中B,最后顺序遍历LinkList命中MD5.完成! |
| 116 | +直接使用adler32值做key,MD5做value也可以,选择合适的散列算法即可. |
| 117 | + |
| 118 | +**HashTable解决了命中率的问题,但是实际中存在大量的missing查找.如何优化?** |
| 119 | + |
| 120 | +zsync在此基础上,使用一个bitbuffer来构建"缺失表",每个分块用3个bit表示,首先做位运算,若存在再访问HashTable,否则直接跳过. |
| 121 | + |
| 122 | +crsync使用2^20的Bloom filter做faster missing. 为什么是2^20? 兼顾速度提升和内存开销(2^20 = 128KB) |
| 123 | + |
| 124 | +#3.client-side rsync |
| 125 | +rsync是C/S架构,依赖rsyncd服务端存在,执行流程是: |
| 126 | + |
| 127 | +1. client生成本地校验表.发送给server |
| 128 | +2. server执行rsync rolling,发送相同块的index和差异块的数据. |
| 129 | +3. client接受指令,执行merge. |
| 130 | + |
| 131 | +是否可以去掉rsyncd server,只依赖HttpServer(CDN)存储文件和校验表,客户端执行rsync rolling? |
| 132 | +crsync和zsync为此而生. |
| 133 | + |
| 134 | +1. 制作新版本文件,生成校验表,一并上传CDN. |
| 135 | +2. client下载新版本的校验表. |
| 136 | +3. client根据本地文件和新校验表,执行rsync rolling,得到重复数据index和差异数据的块index. |
| 137 | +4. client重用本地数据,下载差异数据,合并写入新文件. |
| 138 | +5. done! |
| 139 | + |
| 140 | +#4.crsync VS bs diff/patch |
| 141 | + |
| 142 | +**测试对比1:** |
| 143 | +测试文件: 无尽之剑Android版资源包, base14009.obb ~ base14012.obb 92MB~93MB |
| 144 | +编译版本: MinGW 32bit release |
| 145 | +Bloom filter: 2^20 |
| 146 | +纯本地差异查找与合并,无网络开销 |
| 147 | + |
| 148 | +测试机: DELL PC, Intel i3 550 (4core 3.2GHz) 4GB RAM |
| 149 | + |
| 150 | +| 分块大小 | 校验表生成时间 | 校验块数量 | 差异查找时间 | 重复块数量 | 差异块大小 | |
| 151 | +| :------- | :----------: | :-------: | :---------: | :------: | --------: | |
| 152 | +| 2K | 2182 ms | 46328 | 3753 ms | 40418 | 11820 KB | |
| 153 | +| 4K | 2156 ms | 23164 | 2914 ms | 18960 | 16816 KB | |
| 154 | +| 8K | 2154 ms | 11582 | 2719 ms | 9249 | 18664 KB | |
| 155 | + |
| 156 | +测试机 HP PC, Intel i7-3770 (8core 3.4GHz) 8GB RAM: |
| 157 | + |
| 158 | +| 分块大小 | 校验表生成时间 | 校验块数量 | 差异查找时间 | 重复块数量 | 差异块大小 | |
| 159 | +| :------- | :----------: | :-------: | :---------: | :------: | --------: | |
| 160 | +| 2K | 1745 ms | 46328 | 2974 ms | 40418 | 11820 KB | |
| 161 | + |
| 162 | +**crsync VS bspatch** |
| 163 | + |
| 164 | +| 算法 | 差异量 | 内存开销 | |
| 165 | +| :--- | :----------: | --------: | |
| 166 | +| bsdiff | 16.7MB (diff文件) | 108.7MB (92MB+16.7MB) | |
| 167 | +| crsync | **12.36MB**(校验表835KB + 差异数据11820KB) | **1.76MB** | |
| 168 | + |
| 169 | +**测试对比2:** |
| 170 | +测试文件: [我叫MT online Android标准版](http://update2.locojoy.com/android/IamMT/150413/IamMT_200100100_locojoy_standard_ac_4.5.4.0_4540.apk) 90.7MB ~ [我叫MT online Android高清版](http://update.locojoy.com/Client/Android/MT-A/0324/IamMT_200100100_locojoy_ac_hd_4.5.4.0_4540.apk) 102MB |
| 171 | +(测试版本基于2015.4.4官网下载,目前已变更) |
| 172 | + |
| 173 | + |
| 174 | +测试机 HP PC, Intel i7-3770 (8core 3.4GHz) 8GB RAM: |
| 175 | + |
| 176 | +| 分块大小 | 校验表生成时间 | 差异查找时间 | merge时间 | |
| 177 | +| :------- | :----------: | :---------: | --------: | |
| 178 | +| 2K | 1379 ms | 2950 ms | 90ms | |
| 179 | + |
| 180 | +**crsync VS bs diff/patch** |
| 181 | + |
| 182 | +| 算法 | 差异量 | 内存开销 | |
| 183 | +| :------- | :----------: | --------: | |
| 184 | +| bsdiff | **29.5MB** (diff文件) | 120.2MB (90.7MB+29.5MB) | |
| 185 | +| crsync | 40.8MB | **1.7MB** | |
| 186 | + |
| 187 | +**这里发现, 为什么无尽之剑资源包使用crsync得到的差异量更小?** |
| 188 | +**那么问题来了, 如何设计出适合差异更新的文件格式** |
| 189 | +几种格式对比: |
| 190 | + |
| 191 | +1. 整个文件不压缩,完整下载量大,更新量小,bsdiff更优,因为diff文件使用bzip2压缩格式.crsync使用原始数据http range下载. |
| 192 | +2. 整个文件zip压缩,完整下载量大,更新量更大,这时bsdiff和crsync都无效.因为少量改动就会造成整个文件的大量差异. |
| 193 | +3. 文件内容用zip压缩,再序列化到一起,完整下载量小,更新量小.这时crsync更优,因为bsdiff对zip压缩过的数据处理会生成额外数据量. |
| 194 | + |
| 195 | +UnrealEngine3 Android使用的obb格式为:**文件头**(贴图原始大小, 压缩大小,文件体偏移位置)+**文件体**(贴图内容zip压缩). |
| 196 | +资源更改只影响zip压缩部分和文件头索引内容.对其他未更改资源无影响. |
| 197 | + |
| 198 | +Android APK是zip压缩格式,但是特定后缀名的文件不压缩,常见图片音视频文件为了支持流式read保持原始格式. |
| 199 | +因此crsync对比bsdiff的差异量大些. |
| 200 | + |
| 201 | +#5.源码 |
| 202 | +纯C语言实现, 遵循C99, 核心部分仅2个文件 crsync.h crsync.c 880行代码.static-link libcurl. |
| 203 | +release版Android so 166KB, Windows mingw32 exe 299KB. |
| 204 | +API实现参考curl设计 |
| 205 | + |
| 206 | +```c |
| 207 | +CRSYNCcode crsync_global_init(); |
| 208 | +crsync_handle_t* crsync_easy_init(); |
| 209 | + |
| 210 | +CRSYNCcode crsync_easy_setopt(crsync_handle_t *handle, CRSYNCoption opt, ...); |
| 211 | + |
| 212 | +CRSYNCcode crsync_easy_perform_match(crsync_handle_t *handle); |
| 213 | +CRSYNCcode crsync_easy_perform_patch(crsync_handle_t *handle); |
| 214 | + |
| 215 | +void crsync_easy_cleanup(crsync_handle_t *handle); |
| 216 | +void crsync_global_cleanup(); |
| 217 | +``` |
| 218 | +
|
| 219 | +源码托管在[https://github.com/9468305/crsync](https://github.com/9468305/crsync) |
| 220 | + |
| 221 | +#6.附录 |
| 222 | +- [rsync官网](http://rsync.samba.org/) |
| 223 | +- [rsync算法论文](http://samba.org/~tridge/phd_thesis.pdf) |
| 224 | +- [librsync](http://librsync.sourceforge.net/) |
| 225 | +- [zsync](http://zsync.moria.org.uk/) |
| 226 | +- [BS diff/patch](http://www.daemonology.net/bsdiff/) |
| 227 | +- [Jarsync - a Java rsync implementation](http://sourceforge.net/projects/jarsync/) |
| 228 | +- [rsync教程](http://tutorials.jenkov.com/rsync/index.html) |
| 229 | +- [rsync java教程](http://javaforu.blogspot.com/2014/01/rsync-in-java-quick-and-partial-hack.html) |
| 230 | +- [REBL算法](http://www.research.ibm.com/people/f/fdouglis/papers/rebl.pdf) |
| 231 | +- [TAPER算法](http://www.usenix.org/events/fast05/tech/full_papers/jain/jain.pdf) |
0 commit comments