xiaobaoqiu Blog

Think More, Code Less

Strace

1.strace简介

strace用来跟踪进程执行时的系统调用和所接收的信号.

Linux下进程不能直接访问硬件设备,当进程需要访问硬件设备(比如读取磁盘文件,接收网络数据等等)时,必须由用户态模式切换至内核态模式,通过系统调用访问硬件设备.strace可以跟踪到一个进程产生的系统调用,包括参数,返回值,执行消耗的时间.

参数及意义如下,忽略了一些不大重要的(其实海报看一些没看懂的…):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
OPTIONS
       -c         计数,包括每个系统调用耗时,调用次数,错误次数.注意这里显示的时间是CPU花在内核上的时间.
       -D         跟踪程序作为被跟踪程序的子进程.
       -d          输出strace关于标准错误的调试信息
       -f          根据进程,包括其子进程(fork(2), vfork(2) and clone(2)这些系统调用)
       -ff         如果提供-o filename,则所有进程的跟踪结果输出到相应的filename.pid中,pid是各进程的进程号.
       -F          尝试跟踪vfork调用
       -h         帮助信息.
       -i          输出系统调用的入口指针
       -q         禁止输出attaching和detaching的信息
       -r          打印出每个系统调用的相对时间(相对于第一个系统调用)
       -t          在输出中的每一行前加上时间信息(时分秒)
       -tt         在输出中的每一行前加上时间信息,包含毫秒
       -ttt        微秒级输出,单位是秒(如1454483650.043891).
       -T          显示每个系统调用使用时间的百分比占比.
       -v          输出所有的系统调用.一些调用关于环境变量,状态,输入输出等调用由于使用频繁,默认不输出.
       -V          版本信息
       -x          以十六进制形式输出非标准字符串(non-ASCII strings)
       -xx         所有字符串以十六进制形式输出
       -y          Print paths associated with file descriptor arguments.
       -a column   设置返回值的输出位置,默认为40
       -b syscall  当某个系统调用被调用,trace程序就会detach.当前只支持execve. 这个选项在trace多线程的时候十分有用(需要-f选项)
       -e expr     指定一个表达式,用来控制如何跟踪.表达式格式如下:

                             [qualifier=][!]value1[,value2]...

                   其中qualifier只能是 trace,abbrev,verbose,raw,signal,read,write其中之,value是用来限定的符号或数字.默认的 qualifier是 trace.感叹号是否定符号.例如:
-eopen等价于 -e trace=open,表示只跟踪open调用.而-etrace!=open表示跟踪除了open以外的其他调用.

       -e trace=set   只跟踪指定的系统调用.例如:-e trace=open,close,rean,write表示只跟踪这四个系统调用.默认的为set=all.
       -e trace=file   只跟踪有关文件操作的系统调用
       -e trace=process 只跟踪有关进程控制的系统调用.
       -e trace=network 只跟踪与网络有关的所有系统调用.
       -e trace=signal 只跟踪所有与系统信号有关的 系统调用
       -e trace=ipc 只跟踪所有与进程通讯有关的系统调用
       -e trace=desc 只跟踪所有与文件描述符有关的系统调用
       -e trace=memory 只跟踪所有与内存映射有关的系统调用
       -e signal=set 指定跟踪的系统信号.默认为all.如 signal=!SIGIO(或者signal=!io),表示不跟踪SIGIO信号.
       -o filename 将trace结果输出到文件
       -p pid      跟踪指定的进程pid
       -s strsize  指定输出的字符串的最大长度.默认为32.文件名一直全部输出.
       -S sortby   使用了-c选项的情况下的排序准则,可选包括time, calls, name, 和nothing,默认是time
       -u username 以username的UID和GID执行被跟踪的命令.

使用cheat可以看到strace通常使用方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
xiaobaoqiu@xiaobaoqiu:~$ cheat strace
# 基本使用
strace <command>

# 将trace结构写到文件,使用-o参数
strace -o strace.out <other switches> <command>

# 只trace open()这个系统调用
strace -e trace=open <command>

# trace所有会打开文件的系统调用
strace -e trace=file <command>

# trace所有和进程管理相关的系统调用,再查看一个进程的fork,wait和exec等步骤的时候很有用
strace -e trace=process <command>

# 当前进程fork出来的子进程也trace
strace -f <command>

# 每个系统调用计数,包括:调用时间,调用次数,错误次数
strace -c <command>

# trace某一个进程(可以指定多个pid)
strace -p <pid>

通常而言,我们使用-c选项找出最耗时的系统调用,再使用-e选项跟踪这个操作.

参考: 手把手教你用Strace诊断问题

2.strace使用Demo

一直很好奇top命令的数据从哪里获取的.我这里的前提我知道的一点:top的数据肯定是从proc/pid/下的某个文件中读取的,有这个前提我们就可以来跟踪top命令执行过程中有那些系统调用了,打开了什么文件等.

1
2
3
4
5
6
[baoqiu.xiao@Xxx ~]$ strace -o top.strace -e trace=open top

//找出1381这个进程的信息来源
[baoqiu.xiao@Xxx ~]$ grep 1381 top.strace 
open("/proc/1381/stat", O_RDONLY)       = 4
open("/proc/1381/statm", O_RDONLY)      = 4

从上面的strace中,我们知道,top的信息来源于两个文件,stat和statm.这两个文件的介绍参考上一篇博客: http://xiaobaoqiu.github.io/blog/2016/01/28/who-eat-jvms-memory/.

参考: 8 Options to Trace/Debug Programs using Linux strace Command

火丁笔记 strace

Who Eats JVM's Memory

1.JVM内存占用上限

关于JVM的一些基础知识,可以看 JVM Internals,曾经我尝试翻译过,地址:http://xiaobaoqiu.github.io/blog/2014/09/11/internal-jvm/

这里要说的是,那些参数决定了JVM内存上限,比如常用的内存配置如下:

1
-Xms1024m -Xmx1024m -XX:PermSize=256m

说明堆(Heap)的大小1G,永久代大小256M.

另外,64位的操作系统(Linux服务器),线程的栈空间大小最大为1M:

1
2
3
4
5
6
7
[baoqiu.xiao@Xxx ...]$ java -XX:+PrintFlagsFinal -version | grep ThreadStackSize
     intx CompilerThreadStackSize                   = 0               {pd product}        
     intx ThreadStackSize                           = 1024            {pd product}        
     intx VMThreadStackSize                         = 1024            {pd product}        
java version "1.7.0_45"
Java(TM) SE Runtime Environment (build 1.7.0_45-b18)
Java HotSpot(TM) 64-Bit Server VM (build 24.45-b08, mixed mode)

JVM的占用上限由一下几个因素决定:

1. 堆内存
2. 持久代
3. 线程栈空间
4. 堆外内存

堆外内存参考: http://www.infoq.com/cn/news/2014/12/external-memory-heap-memory

线程数可以如下得到:

1
2
[baoqiu.xiao@Xxx ~]$ ps -m 1381 | wc -l
176

通常而言,我们只需要计算堆内存,持久代再加上线程栈空间.比如我们的本地一个小服务:

1
Max Memory = Heap(1G) + Perm(256M) + 176 * Thread stack (1M)

2.JVM RES数据来源

首先得知道top命令中RES数据从哪里来的.再Linux上,万物皆文件,因此RES的数据也是来源于文件.

2.1 /proc/pid内容

下面是一个典型的Tomcat应用的线程下的内容,进程号为1381,简单说描述每个文件的内容或者作用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
attr         进程的属性
autogroup
auxv
cgroup
clear_refs
cmdline       启动进程时执行的命令
coredump_filter
cpuset
cwd           指向进程当前工作目录的软链
environ       进程执行时使用的环境变量
exe           这个就是起这个进程的执行文件
fd            进程打开的文件描述符,可以知道具体的文件路径
fdinfo
io            进程的io统计信息
limits            进程的软限制,硬限制等信息
loginuid
maps          进程相关的内存映射信息
mem           代进程持有的内存,不可读
mountinfo
mounts
mountstats
net
numa_maps
oom_adj       调节oom-killer的参数
oom_score         oom-killer打分,当需要是,oom-killer会根据各个进程的分数,kill掉某个进程
oom_score_adj         调节oom-killer的参数
pagemap       进程的虚拟页和物理内存页或者swap区的映射关系
personality
root          指向进程根目录的软链
sched
schedstat
sessionid
smaps             This file shows memory consumption for each of the process's mappings.
stack             This file provides a symbolic trace of the function calls in this process's kernel stack
stat          进程的状态
statm         进程使用的内存的状态
status            进程状态信息,比stat/statm更具可读性
syscall
task          进程包含的线程,子目录名是线程的ID
wchan

参考: http://man7.org/linux/man-pages/man5/proc.5.html

2.2 /proc/pid下内存相关文件

和内存相关的文件主要包括(解析各个文件的时间不同,可能内容上存在对不上的情况):

1. statm: 
2. stat:    
3. status
4. maps
5. smaps

1. statm

进程的内存使用,注意单位是page,内容的解析参考2.3节的内容.

2. stat

1381这个进程的stat文件及各个字段的含义(含义参见proc命令手册: man proc –> 搜stat, 见/proc/[pid]/stat):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
1381 pid             进程号
(java)    comm            应用程序或命令的名字
S state           任务的状态(RSDZTW中一个字符),R:runnign, S:sleeping in an interruptible wait, D:waiting in uninterruptible disk sleep, Z:zombie, T is  traced  or  stopped, W is paging
1 ppid            父进程ID
1374  pgrp            线程组号
1374  session         该任务所在的会话组ID
0 tty_nr          进程的控制终端设备号
-1    tpgid           进程控制终端的前台任务id
4202496   flags           进程内核标志位
988135    minflt          任务不需要从硬盘拷数据而发生的缺页(minor faults)的次数
191   cminflt         任务的所有的waited-for子进程曾经发生的次缺页的次数累计值
252944    majflt          任务需要从硬盘拷数据而发生的缺页(minor faults)的次数
5 cmajflt             任务的所有的waited-for子进程需要从硬盘拷数据而发生的缺页(minor faults)的次数
229942    utime           任务在用户态(User mode)运行的时间,单位为时钟周期(clock ticks)
102219    stime           任务在内核态(kernel mode)运行的时间,单位为时钟周期(clock ticks)
0 cutime          任务的所有的waited-for子进程曾经在用户态运行的时间累计值,单位为时钟周期(clock ticks)
0 cstime          任务的所有的waited-for子进程曾经在核心态运行的时间累计值,单位为时钟周期(clock ticks)
20    priority            任务的动态优先级
0 nice            任务的静态优先级
179   num_threads     进程的线程数
0 itrealvalue     由于计时间隔导致的下一个 SIGALRM 发送进程的时延,单位jiffies
2107287914    starttime       任务启动的时间,单位为jiffies
3968053248    vsize           该任务的虚拟地址空间大小,单位byte
122542    rss             任务当前驻留物理地址空间的大小,单位page
18446744073709551615  rsslim          任务能驻留物理地址空间的最大值,单位byte
4194304   startcode       任务在虚拟地址空间的代码段的起始地址
4196452   endcode     任务在虚拟地址空间的代码段的结束地址
140734635670848   startstack      任务在虚拟地址空间的栈的结束地址
140734635653408   kstkesp         ESP(stack pointer, 栈指针)的当前值
250460799149  kstkeip         EIP(instruction pointer, 指令指针)的当前值
0 signal          pending信号的位图(bitmap),十进制数显示
0 blocked         blocked信号的位图(bitmap),十进制数显示
2 sigignore       ignored信号的位图(bitmap),十进制数显示
16800973  sigcatch        caught信号的位图(bitmap),十进制数显示
18446744073709551615  wchan           进程等待的channel.是系统调用地址
0 nswap           被swapped的页数(当前不用)
0 cnswap      所有子进程被swapped的页数的和(当前不用)
17    exit_signal     该进程结束时,向父进程所发送的信号
3 processor       进程最后一次执行的CPU号
0 rt_priority     实时调度优先级
0 policy          调度策略
4 delayacct_blkio_ticks   IO阻塞延迟汇总,单位时钟周期(clock ticks)
0 guest_time      进程的guest time(指费再运行guest操作系统上的时间),单位时钟周期(clock ticks)
0 cguest_time     子进程guest time

里面包含我们熟悉的RSS数据,注意其单位是page.

3. status

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
[baoqiu.xiao@Xxx /proc/1381]$ sudo cat status
Name: Java                                        /*当前进程的命令*/
State:    S (sleeping)                           /*当前进程状态: "R (running)", "S (sleeping)", "D (disk sleep)", "T (stopped)", "T (tracing stop)", "Z (zombie)", or "X (dead)"*/
Tgid: 1381                                      /*线程组号*/
Pid:  1381                                             /*线程id*/
PPid: 1                                            /*父进程的pid*/
TracerPid:    0                                     /*跟踪进程的pid,如果没有就是0*/
Uid:  40001   40001   40001   40001      /*uid euid suid fsuid*/
Gid:  40001   40001   40001   40001      /*gid egid sgid fsgid*/
Utrace:   0
FDSize:   512                                        /*FDSize是当前分配的文件描述符*/
Groups:   40001                             /*这里的groups表示启动这个进程的用户所在的组*/
VmPeak:    3880116 kB                   /*当前进程运行过程中占用内存的峰值*/
VmSize:    3875052 kB                         /*进程现在正在占用的内存*/
VmLck:           0 kB                               /*进程已经锁住的物理内存的大小.锁住的物理内存不能交换到硬盘*/
VmHWM:      766000 kB                    /*程序得到分配到物理内存的峰值*/
VmRSS:      490168 kB                          /*程序现在使用的物理内存*/
VmData:    3718132 kB                   /*进程数据段的大小*/
VmStk:          88 kB                              /*进程堆栈段的大小*/
VmExe:           4 kB                              /*进程代码的大小*/
VmLib:       15720 kB                          /*进程所使用LIB库的大小*/
VmPTE:        2280 kB                           /*进程所使用LIB库的大小*/
VmSwap:     303616 kB                   /*进程占用Swap的大小*/
Threads:  179                                /*当前进程组线程数*/
SigQ: 0/62812                               /*表示当前待处理信号的个数*/
SigPnd:   0000000000000000             /*屏蔽位,存储了该线程的待处理信号,等同于线程的PENDING信号*/
ShdPnd:   0000000000000000      /*屏蔽位,存储了该线程组的待处理信号.等同于进程组的PENDING信号*/
SigBlk:   0000000000000000             /*放被阻塞的信号,等同于BLOCKED信号*/
SigIgn:   0000000000000002            /*存放被忽略的信号,等同于IGNORED信号*/
SigCgt:   2000000181005ccd            /*存放捕获的信号,等同于CAUGHT信号*/
CapInh:   0000000000000000
CapPrm:   0000000000000000
CapEff:   0000000000000000
CapBnd:   ffffffffffffffff
Cpus_allowed: f
Cpus_allowed_list:    0-3
Mems_allowed: 00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000001
Mems_allowed_list:    0
voluntary_ctxt_switches:  35
nonvoluntary_ctxt_switches:   9

关于其中的TracerPid,在Linux下,我们可以使用strace来跟踪命令的运行.

参考: http://blog.csdn.net/zjl410091917/article/details/8075691

4. maps

maps内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
[baoqiu.xiao@l-crm3.des.dev.cn0 ~]$ head -30 maps
00400000-00401000 r-xp 00000000 fc:07 1179664                            /home/q/java/jdk1.7.0_45/bin/java
00600000-00601000 rw-p 00000000 fc:07 1179664                            /home/q/java/jdk1.7.0_45/bin/java
00b78000-00bed000 rw-p 00000000 00:00 0                                  [heap]
aff80000-100000000 rw-p 00000000 00:00 0 
3a4fe00000-3a4fe20000 r-xp 00000000 fc:02 2613                           /lib64/ld-2.12.so
3a5001f000-3a50020000 r--p 0001f000 fc:02 2613                           /lib64/ld-2.12.so
3a50020000-3a50021000 rw-p 00020000 fc:02 2613                           /lib64/ld-2.12.so
3a50021000-3a50022000 rw-p 00000000 00:00 0 
3a50200000-3a5038a000 r-xp 00000000 fc:02 25297                          /lib64/libc-2.12.so
3a5038a000-3a50589000 ---p 0018a000 fc:02 25297                          /lib64/libc-2.12.so
3a50589000-3a5058d000 r--p 00189000 fc:02 25297                          /lib64/libc-2.12.so
3a5058d000-3a5058e000 rw-p 0018d000 fc:02 25297                          /lib64/libc-2.12.so

第一列代表内存段的虚拟地址 第二列代表执行权限虚拟内存的权限,r=读,w=写,x=,s=共享,p=私有 第三列代表在进程地址里的偏移量 第四列映射文件的主设备号和次设备号 第五列映像文件的节点号,即inode 第六列是映像文件的路径

参考: http://stackoverflow.com/questions/1401359/understanding-linux-proc-id-maps

5. smaps

smaps代表进程的各个线程的内存消耗

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
[baoqiu.xiao@Xxx /proc/1381]$ sudo cat smaps
在smaps文件中,每一条记录(每块)表示进程虚拟内存空间中一块连续的区域.

...
00b78000-00bed000 rw-p 00000000 00:00 0                                  [heap]       /*和maps中的内容相同*/
Size:                468 kB                   /*表示该映射区域在虚拟内存空间中的大小*/
Rss:                  76 kB                    /*表示该映射区域当前在物理内存中实际占用了多少空间*/
Pss:                  76 kB                    /*该虚拟内存区域平摊计算后使用的物理内存大小*/
Shared_Clean:          0 kB             /*和其他进程共享的未被改写的page的大小*/
Shared_Dirty:          0 kB              /*和其他进程共享的被改写的page的大小*/
Private_Clean:         0 kB              /*未被改写的私有页面的大小*/
Private_Dirty:        76 kB
Referenced:           76 kB
Anonymous:            76 kB
AnonHugePages:         0 kB
Swap:                144 kB                 /*由于物理内存不足被swap到交换空间的大小*/
KernelPageSize:        4 kB             /*操作系统一个页面大小*/
MMUPageSize:           4 kB           /*CPU页面大小, MMU表示内存管理单元(Memory Management Unit)*/
...

关于其中的Pss,因为进程之间存在内存共享,比如该区域所映射的物理内存部分同时也被另一个进程映射了,且该部分物理内存的大小为1000KB(Rss会是1000KB),那么该进程分摊其中一半的内存,即Pss=500KB。

内存页引用计数>1,则表示是shared,否则是private.

参考: stackoverflow: what does pss mean in /proc/pid/smaps

Getting information about a process' memory usage from /proc/pid/smaps

另外,当前系统的page大小可以通过getconf命令获取,比如我当前的服务器page大小为4k:

1
2
[baoqiu.xiao@Xxx ~]$ getconf PAGESIZE
4096

2.3 top命令RES数据来源

top命令RES数据来源于/proc/PID/statm中的第二个字段.还是1381这个进程的statm文件内容和对应的top信息如下:

1
2
3
4
5
6
[baoqiu.xiao@Xxx /proc/1381]$ sudo cat statm
968763 122480 558 1 0 929555 0

[baoqiu.xiao@Xxx ~]$ top -p 1381
  PID  USER      PR   NI  VIRT     RES     SHR  S  %CPU  %MEM    TIME+       COMMAND                                                                                                                             
 1381 tomcat    20   0   3784m  478m 2228  S  2.0       6.1           54:53.36    java

/proc/[pid]/statm文件的几个字段的具体意义如下(注意单位是page):

1
2
3
4
5
6
7
size         (1) total program size(same as VmSize in /proc/[pid]/status)
resident      (2) (resident set size)(same as VmRSS in /proc/[pid]/status),即RSS
share         (3) shared pages (i.e., backed by a file)
text          (4) text (code)
lib               (5) library (unused in Linux 2.6)
data          (6) data + stack
dt                (7) dirty pages (unused in Linux 2.6)

可以验证,1381这个进程的RES大小来源, 和top命令中RES的数字是吻合的:

1
122480 * 4096 / (1024 * 1024) = 478.4375 MB

3.谁吃了JVM内存

写到这发现谢步下去了,因为我一开始就写错了.

想知道谁吃了我们JVM的内存,可以jmap dump下来,用MAT分析一下.

想知道谁吃了服务器的内存,建议看看参考里面的Linux Used内存到底哪里去了这篇文章.

参考: Linux Memory Management Overview

Understanding Process memory

How much memory are applications really using

Memory Management

Linux Used内存到底哪里去了

也看linux内存去哪儿了

Protocol Buffers

1.Protocol Buffers简介

最近项目中接触到久闻大名的Protocol Buffers。简单学习Protocol Buffers是什么,能做什么,为什么需要它,怎么使用它。

Protocol Buffers官网:https://developers.google.com/protocol-buffers/

Protocol Buffers官网(中文):https://developers.google.com/protocol-buffers/?hl=zh-CN

Git地址:https://github.com/google/protobuf

Java API:https://developers.google.com/protocol-buffers/docs/reference/java/?hl=zh-CN

proro文件的编写指南:https://developers.google.com/protocol-buffers/docs/style?hl=zh-CN

Java使用指南:https://developers.google.com/protocol-buffers/docs/javatutorial?hl=zh-CN

protocol buffers是google提供的一种将结构化数据进行序列化和反序列化的方法,其优点是语言中立,平台中立,可扩展性好,目前在google内部大量用于数据存储,通讯协议等方面。语言中立说明和语言无关,可以作为跨语言的通信协议。平台中立说明Protocol Buffers可以作为不同平台之间的通信协议;可扩展性好可以理解为protocol buffers支持嵌套。

PB在功能上类似XML,但是序列化后的数据更小,解析更快,使用上更简单。用户只要按照proto语法在.proto文件中定义好数据的结构,就可以使用PB提供的工具(protoc)自动生成处理数据的代码,使用这些代码就能在程序中方便的通过各种数据流读写数据。

PB目前支持Java, C++和Python3种语言(截止目前已经额外支持C#,Ruby和Object-C)。另外,PB还提供了很好的向后兼容,即旧版本的程序可以正常处理新版本的数据,新版本的程序也能正常处理旧版本的数据。

2.Protocol Buffers安装

1.下载源码 从https://github.com/google/protobuf%E4%B8%8B%E8%BD%BD%E6%BA%90%E7%A0%81

2.根据README安装 (1).使用./autogen.sh生成configure脚本; (2).配置./configure,默认在/usr/local下,可以指定路径 (3).make (4).make check (5).make install

这里安装的是最新的Protoc 3。安装之后,就会有一个根据Message文件产生对应代码文件的工具protoc:

1
2
xiaobaoqiu@xiaobaoqiu:~/Work/JavaCode/ProtobufDemo/com/qunar/scm/protobuf$ protoc --version
libprotoc 3.0.0

在帮助文当中就可以看到如何生成各种类型的文件的方式:

1
2
3
4
5
6
7
  --cpp_out=OUT_DIR           Generate C++ header and source.
  --csharp_out=OUT_DIR        Generate C# source file.
  --java_out=OUT_DIR          Generate Java source file.
  --javanano_out=OUT_DIR      Generate Java Nano source file.
  --objc_out=OUT_DIR          Generate Objective C header and source.
  --python_out=OUT_DIR        Generate Python source file.
  --ruby_out=OUT_DIR          Generate Ruby source file.

3.Protocol Buffers Demo in Java

参考官网的一个例子,message是定义protobuf的关键词,Person表示一个人的信息,包括id,name,email和phone四个信息,其中name和id是必须的,email是可选的,phone是数组形式并且phone本身也是一种消息(message),PhoneNumber内部包含了枚举类型PhoneType,主要枚举的定义和Java有点不同,这里是分号分割的。1234这种序号表示字段在数据中的顺序,新增字段依次增加这个序号就行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package tutorial;

option java_package = "com.example.tutorial";
option java_outer_classname = "AddressBookProtos";

message Person {
  required string name = 1;
  required int32 id = 2;
  optional string email = 3;

  enum PhoneType {
    MOBILE = 0;
    HOME = 1;
    WORK = 2;
  }

  message PhoneNumber {
    required string number = 1;
    optional PhoneType type = 2 [default = HOME];
  }

  repeated PhoneNumber phone = 4;
}

message AddressBook {
  repeated Person person = 1;
}

string和int32是protobuf支持的基本类型,所有支持的类型及其含义如下:

如下生成message对应的Java文件如下:

1
protoc -I=. --java_out=. ./addressbook.proto

生产数据方直接使用对应的Builder生成对象,然后往流里面写数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
    final String FILE_NAME = "protobuf.stream";

    //build
    AddressBookProtos.Person me =
            AddressBookProtos.Person.newBuilder()
                    .setId(1234)
                    .setName("baoiu.xiao")
                    .setEmail("baoiu.xiao@example.com")
                    .addPhone(AddressBookProtos.Person.PhoneNumber.newBuilder()
                            .setNumber("15910645718")
                            .setType(AddressBookProtos.Person.PhoneType.HOME))
                    .build();

    FileOutputStream FileOutputStream = new FileOutputStream(FILE_NAME);
    //往流里面写数据
    me.writeTo(FileOutputStream);

消费数据方可以直接从流中读取并解析对象:

1
2
3
4
5
6
7
8
    final String FILE_NAME = "protobuf.stream";

    FileInputStream fis = new FileInputStream(FILE_NAME);

    //从流里面解析出AddressBookProtos.Person对象
    AddressBookProtos.Person me = AddressBookProtos.Person.parseFrom(fis);

    System.out.println(me.toString());

4.自描述消息

5.序列化性能

参考git上的各种序列化的测试结果: https://github.com/eishay/jvm-serializers/wiki

Memcached存储枚举

今天发布线上碰到一个小问题,Memcached存储的数据类型包括枚举的时候,当修改枚举的包路径之后,会出现问题。 异常信息如下,比较直观,就是数据的反序列化失败:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
[2015-11-23 18:04:45 ERROR net.rubyeye.xmemcached.transcoders.BaseSerializingTranscoder:113] Caught CNFE decoding 944 bytes of data
java.lang.ClassNotFoundException: Xxx.Xxx.Xxx.Xxx.RoomStatus
        at org.apache.catalina.loader.WebappClassLoader.loadClass(WebappClassLoader.java:1702) ~[catalina.jar:7.0.47]
        at org.apache.catalina.loader.WebappClassLoader.loadClass(WebappClassLoader.java:1547) ~[catalina.jar:7.0.47]
        at net.rubyeye.xmemcached.transcoders.BaseSerializingTranscoder$1.resolveClass(BaseSerializingTranscoder.java:102) ~[xmemcached-1.4.1.jar:na]
        at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1612) ~[na:1.7.0_45]
        at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1517) ~[na:1.7.0_45]
        at java.io.ObjectInputStream.readEnum(ObjectInputStream.java:1725) ~[na:1.7.0_45]
        at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1347) ~[na:1.7.0_45]
        at java.io.ObjectInputStream.defaultReadFields(ObjectInputStream.java:1990) ~[na:1.7.0_45]
        at java.io.ObjectInputStream.readSerialData(ObjectInputStream.java:1915) ~[na:1.7.0_45]
        at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:1798) ~[na:1.7.0_45]
        at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1350) ~[na:1.7.0_45]
        at java.io.ObjectInputStream.readObject(ObjectInputStream.java:370) ~[na:1.7.0_45]
        at java.util.ArrayList.readObject(ArrayList.java:771) ~[na:1.7.0_45]
        at sun.reflect.GeneratedMethodAccessor41.invoke(Unknown Source) ~[na:na]
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.7.0_45]
        at java.lang.reflect.Method.invoke(Method.java:606) ~[na:1.7.0_45]
        at java.io.ObjectStreamClass.invokeReadObject(ObjectStreamClass.java:1017) ~[na:1.7.0_45]
        at java.io.ObjectInputStream.readSerialData(ObjectInputStream.java:1893) ~[na:1.7.0_45]
        at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:1798) ~[na:1.7.0_45]
        at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1350) ~[na:1.7.0_45]
        at java.io.ObjectInputStream.defaultReadFields(ObjectInputStream.java:1990) ~[na:1.7.0_45]
        at java.io.ObjectInputStream.readSerialData(ObjectInputStream.java:1915) ~[na:1.7.0_45]
        at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:1798) ~[na:1.7.0_45]
        at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1350) ~[na:1.7.0_45]
        at java.io.ObjectInputStream.readObject(ObjectInputStream.java:370) ~[na:1.7.0_45]
        at net.rubyeye.xmemcached.transcoders.BaseSerializingTranscoder.deserialize(BaseSerializingTranscoder.java:106) ~[xmemcached-1.4.1.jar:na]
        at net.rubyeye.xmemcached.transcoders.SerializingTranscoder.decode0(SerializingTranscoder.java:92) [xmemcached-1.4.1.jar:na]
        at net.rubyeye.xmemcached.transcoders.SerializingTranscoder.decode(SerializingTranscoder.java:86) [xmemcached-1.4.1.jar:na]
        at net.rubyeye.xmemcached.XMemcachedClient.fetch0(XMemcachedClient.java:630) [xmemcached-1.4.1.jar:na]
        at net.rubyeye.xmemcached.XMemcachedClient.get0(XMemcachedClient.java:1030) [xmemcached-1.4.1.jar:na]
        at net.rubyeye.xmemcached.XMemcachedClient.get(XMemcachedClient.java:988) [xmemcached-1.4.1.jar:na]
        at net.rubyeye.xmemcached.XMemcachedClient.get(XMemcachedClient.java:999) [xmemcached-1.4.1.jar:na]

场景如下:

  1. Memcached已经有数据,数据中包含RoomStatus数据;
  2. 修改了枚举的包路径,发布线上;
  3. 从Cache中读取之前的数据会出现枚举反序列化异常;

在本地代码中重现了一下问题,并看了一下使用枚举的情况下其他可能处心问题的场景,结论如下(前提是Cache中已经存在使用RoomStatus数据):

  1. 修改包路径,会导致get数据反序列化失败,java.lang.ClassNotFoundException: Xxx.Xxx.Xxx.Xxx.RoomStatus;
  2. RoomStatus增加项,正常;
  3. RoomStatus减少项(已Cache数据中未使用到的项),正常;
  4. RoomStatus减少项(已Cache数据中使用到的项),异常:enum constant Xxx does not exist in Xxx.Xxx.Xxx.Xxx.RoomStatus;

综上,在Memcached中cache包含枚举的数据时候,建议如下:

  1. 不要修改枚举的包路径;
  2. 不要删除枚举中的数据;

这些序列化和反序列化的逻辑再XMemcached客户端实现的。详见SerializingTranscoder类,包含数据的encode和decode逻辑。再使用Memcached的时候也可以使用

局部敏感Hash

1.LSH简介

之前在项目中做数据聚合去重的逻辑的时候简单看过局部敏感Hash(Locality Sensitive Hashing,简称LSH)这个东东。今天整理一下个人的理解。

LSH可以理解为一种衡量文本相似度的算法,特点是散列前的相似点经过哈希之后,也能够在一定程度上相似,并且具有一定的概率保证。其有坚实的理论依据(98年左右理论就提出来了,99年有第一版实现)并且在高维数据空间中表现优异。简单的价格实验场景:

  1. 近似检测(Near-duplicate detection): 通常运用在网页去重方面。在搜索中往往会遇到内容相似的重复页面,它们中大多是由于网站之间转载造成的。可以对页面计算LSH,通过查找相等或相近的LSH值找到Near-duplicate。
  2. 图像、音频检索: 通常图像、音频文件都比较大,并且比较起来相对麻烦,我们可以事先对其计算LSH,用作信息指纹,这样可以给定一个文件的LSH值,快速找到与其相等或相近的图像和文件。
  3. 聚类: 将LSH值作为样本特征,将相同或相近的LSH值的样本合并在一起作为一个类别。
  4. 指纹匹配: 一个手指指纹通常由一些细节来表征,通过对比较两个手指指纹的细节的相似度就可以确定两个指纹是否相同或相似。

LSH的发展历史可以参考: http://jacoxu.com/?p=496

2.普通Hash

说到Hash,大家都很熟悉,是一种典型的Key-Value结构,最常见的算法莫过于MD5。其设计思想是使Key集合中的任意关键字能够尽可能均匀的变换到Value空间中,不同的Key对应不同的Value。通过建立Hash的方式我们能够得到O(1)的查找时间性能,其中关键在于选取一个hash function(md5就是一致hash function)。

md5这种hash函数通常情况下,Key值只有轻微变化,Value值也会发生很大地变化。比如下面实验中用到的文本,仅仅是邮箱号少了个.,其md5完全不同:

1
2
3
4
5
6
7
8
xiaobaoqiu@xiaobaoqiu:~/temp/md5$ cat 1.dat 
xiaobaoqiu@qunar.com
xiaobaoqiu@xiaobaoqiu:~/temp/md5$ cat 2.dat 
xiaobaoqiu@qunarcom
xiaobaoqiu@xiaobaoqiu:~/temp/md5$ md5sum 1.dat 
ca201d44a9bb6f8e0ca761cdeb678948  1.dat
xiaobaoqiu@xiaobaoqiu:~/temp/md5$ md5sum 2.dat 
f585aa440eb3b8bbc46f1184e2944fb9  2.dat

原始文本是极其相似的,但是hash之后这种相似性就丢失了。

3.LSH

局部敏感哈希的最大特点就在于保持数据的相似性。需要注意的是这里说的保持数据的相似度不是说保持100%的相似度,而是保持最大可能的相似度。换个角度来看,可以将LSH理解为数据降维的方法。

数据对应的维度越高,信息量也就越大,相反,如果数据进行了降维,那么毫无疑问数据所反映的信息必然会有损失。哈希函数从本质上来看就是一直在扮演数据降维的角色。

LSH的基本思想是:将原始数据空间中的两个相邻数据点通过相同的映射或投影变换后,这两个数据点在新的数据空间中仍然相邻的概率很大,而不相邻的数据点被映射到同一个桶的概率很小。

参考: https://en.wikipedia.org/wiki/Locality-sensitive_hashing http://www.cnblogs.com/maybe2030/p/4953039.html http://blog.csdn.net/weiyuweizhi/article/details/8921973