如何写出让CPU跑得更快的代码


如何写出让 CPU 跑得更快的代码?
————NEOHOPE的代码优化手册

一、需求阶段

1、过早优化是魔鬼。业务能运行,比业务响应速度更重要。能用的慢代码,比不能用的快代码好的多

2、搞清楚业务类型,是重IO,还是重CPU,还是重GPU,明确具体需求

3、这个需求,确定是要通过代码优化解决吗?
是不是一个业务问题无法解决,想通过系统去限制业务呢?

4、这个需求,是否可以通过花钱或其他方式解决,比如增加硬件

5、搞清楚为何要优化代码,不优化问题是什么?优化代码的风险是什么?时间周期大概是剁手?代价和受益如何?

6、对比4、5的各种方案,确定用哪个

二、分析阶段

1、一旦明确了要进行优化,不要一头扎入细节。考虑整个系统的架构是否合理,看下全局,看下上下游的影响。

2、遵从Ahmdal定律,确定优化范围;

3、咱们要优化的东西,业界是否已有解决方案,不要重复造轮子

4、如果涉及面比较广,别急着动手,做好分析,制定方案,再动手

三、架构阶段

1、调整不合理的架构

2、调整不合理的数据流

3、进行必要的组件替换

四、算法提升阶段

1、遵从80-20法则,程序80%的时间在运行20%或更少的代码,针对热代码进行优化,才容易产出效果;

2、评估当前算法,是否有高效的算法
比如:用DP或贪婪算法替代暴力算法,将算法的时间复杂度尽量降低

3、使用合理的数据结构
比如哈希表、红黑树

4、使用数学公式,替代低效计算

5、缓存重复出现的中间结果,空间换时间

五、编码提升阶段

1、遵从数据访问的局部性法则,按数据存放顺序访问内存效率远高于乱序访问内存效率,也就是尽量帮助CPU做好数据Cache的预测工作。同样根据Cache大小,做好数据结构的优化工作,进行数据压缩或数据填充,也是提升Cache效率的好方式;

2、遵从指令访问的局部性法则,减少跳转指令,同样是尽量帮助CPU做好数据Cache的预测工作;现代CPU都有一些预测功能【如分支预测】,利用好CPU的这些功能,也会提升Cache命中率;

3、减少内存的重复申请和释放

4、 减少函数调用层数
使用INLINE等关键字,同样是减少调用层数
使用宏展开,也有类似效果

5、减少数据传递个数,合理封装

6、数据传递,以引用为主,减少传值

7、减少数据类型转换

10、调整运算顺序,减少CPU低效运算。
比如除法运算。

11、调整运算顺序,尽早结束循环,避免无效操作

12、 少用高级语言的多重继承等特性

13、使用汇编和C语言,而不是更高级的语言,很多时候可以提速运算效率;

14、直接使用昂贵的寄存器作为变量,可以变相提供加速效果;

15、采用性能高的函数,批量处理数据,如memset等

16、能在编译阶段完成的任务,不要留到运行阶段完成。
比如,CPP的泛型是编译阶段完成的,比运行时再做处理的语言效率更高
比如,不要用反射,不要动态类型绑定
比如,初始化工作尽早完成

六、编译阶段

1、开启编译器优化,速度最大化

2、使用Intel自家的编译器,开启性能优化,很多时候可以提速运算效率;

3、考虑指令级并行性(IPL)

七、运行阶段

1、避免计算线程在多个核心之间漂移,避免缓存重复加载,可以绑定核心【物理核即可,不用到逻辑核】,提高效率;

2、去除伪共享缓存:在多核环境下,减少多个核心对同一区域内存的读写并发操作,减少内存失效的情况的发生;

3、合理提高进程优先级,减少进程间切换,可以变相提供Cache提速的效果;

4、关闭Swap,可以变相提供内存提速、Cache提速的效果;

八、测试阶段

1、合适的手段及工具进行性能评估

2、要符合实际情况,不要片面追求指标

先总结这些吧。

Linux启动过程02

操作系统的启动分为两个阶段:引导boot和启动startup,本文主要描述startup过程。

接上文Grub在/boot目录下找到的linux内核,是bzImage格式
1、bzImage格式生成:
1.1、head_64.S+其他源文件->编译-> vmlinux【A】
1.2、objcopy工具拷贝【 拷贝时,删除了文件中“.comment”段,符号表和重定位表】->vmlinux.bin【A】
1.3、gzib压缩->vmlinux.bin.gz
1.4、piggy打包,附加解压信息->piggy.o->其他.o文件一起链接->vmlinux【B】
1.5、objcopy工具拷贝【 拷贝时,删除了文件中“.comment”段,符号表和重定位表】->vmlinux【B】
1.6、head.S +main.c+其他->setup.bin
1.7、setup.bin+vmlinux.bin【B】->bzImage合并->bzImage

2、GRUB加载bzImage文件
2.1、会将bzImage的setup.bin加载到内存地址0x90000 处
2.2、把vmlinuz中的vmlinux.bin部分,加载到1MB 开始的内存地址

3、GRUB会继续执行setup.bin代码,入口在header.S【arch/x86/boot/header.S】
GRUB会填充linux内核的一个setup_header结构,将内核启动需要的信息,写入到内核中对应位置,而且GRUB自身也维护了一个相似的结构。
Header.S文件中从start_of_setup开始,其实就是这个setup_header的结构。
此外, bootparam.h有这个结构的C语言定义,会从Header.S中把数据拷贝到结构体中,方便后续使用。

4、GRUB然后会跳转到 0x90200开始执行【恰好跳过了最开始512 字节的 bootsector】,正好是head.S的_start这个位置;

5、在head.S最后,调用main函数继续执行

6、main函数【 arch/x86/boot/main.c】【16 位实模式】
6.1、拷贝header.S中setup_header结构,到boot_params【arch\x86\include\uapi\asm\bootparam.h】
6.2、调用BIOS中断,进行初始化设置,包括console、堆、CPU模式、内存、键盘、APM、显卡模式等
6.3、调用go_to_protected_mode进入保护模式

7、 go_to_protected_mode函数【 arch/x86/boot/pm.c】
7.1、安装实模式切换钩子
7.2、启用1M以上内存
7.3、设置中断描述符表IDT
7.4、设置全局描述符表GDT
7.4、protected_mode_jump,跳转到boot_params.hdr.code32_start【保护模式下,长跳转,地址为 0x100000】

8、恰好是vmlinux.bin在内存中的位置,通过这一跳转,正式进入vmlinux.bin

9、startup_32【arch/x86/boot/compressed/head64.S】
全局描述符GDT
加载段描述符
设置栈
检查CPU是否支持长模式
开启PAE
建立MMU【4级,4G】
开启长模式
段描述符和startup_64地址入栈
开启分页和保护模式
弹出段描述符和startup_64地址到CS:RIP中,进入长模式

10、 startup_64【arch/x86/boot/compressed/head64.S】
初始化寄存器
初始化栈
调准给MMU级别
压缩内核移动到Buffer最后
调用.Lrelocated

11、.Lrelocated
申请内存
被解压数据开始地址
被解压数据长度
解压数据开始地址
解压后数据长度
调用 extract_kernel解压内核

12、extract_kernel解压内核【arch/x86/boot/compressed/misc.c】
保存boot_params
解压内核
解析ELF,处理重定向, 把 vmlinux 中的指令段、数据段、BSS 段,根据 elf 中信息和要求放入特定的内存空间
返回了解压后内核地址,保存到%rax

13、返回到.Lrelocated继续执行
跳转到%rax【解压后内核地址】,继续执行
解压后的内核文件,入口函数为【arch/x86/kernel/head_64.S】

14、SYM_CODE_START_NOALIGN(startup_64)【arch/x86/kernel/head_64.S】
SMP 系统加电之后,总线仲裁机制会选出多个 CPU 中的一个 CPU,称为 BSP,也叫第一个 CPU。它负责让 BSP CPU 先启动,其它 CPU 则等待 BSP CPU 的唤醒。
第一个启动的 CPU,会跳转 secondary_startup_64 函数中 1 标号处,对于其它被唤醒的 CPU 则会直接执行 secondary_startup_64 函数。

15、secondary_startup_64 函数【arch/x86/kernel/head_64.S】
各类初始化工作,gdt、描述符等
跳转到initial_code,也就是x86_64_start_kernel

16、 x86_64_start_kernel【arch/x86/kernel/head64.c】
各类初始化工作,清理bss段,清理页目录,复制引导信息等
调用x86_64_start_reservations

17、x86_64_start_reservations【arch/x86/kernel/head64.c】
调用start_kernel();

18、start_kernel【init/main.c】
各类初始化:ARCH、日志、陷阱门、内存、调度器、工作队列、RCU锁、Trace事件、IRQ中断、定时器、软中断、ACPI、fork、缓存、安全、pagecache、信号量、cpuset、cgroup等等
调用 arch_call_rest_init,调用到rest_init

19、rest_init【init/main.c】
kernel_thread,调用_do_fork,创建了kernel_init进程,pid=1 . 是系统中所有其它用户进程的祖先
kernel_thread,调用_do_fork,创建了 kernel_thread进程,pid=2, 负责所有内核线程的调度和管理
当前的进程, 最后会变成idle进程,pid=0

20、kernel_init
根据内核启动参数,调用run_init_process,创建对应进程
调用try_to_run_init_process函数,尝试以 /sbin/init、/etc/init、/bin/init、/bin/sh 这些文件为可执行文件建立init进程,只要其中之一成功就可以

调用链如下:

try_to_run_init_process
run_init_process
kernel_execve
bprm_execve
exec_binprm
search_binary_handler-》依次尝试按各种可执行文件格式进行加载,而ELF的处理函数为 load_elf_binary
load_elf_binary
start_thread
start_thread_common,会将寄存器地址,设置为ELF启动地址
当从系统调用返回用户态时,init进程【1号进程】,就从ELF执行了

到此为止,系统的启动过程结束。

Linux启动过程01【UEFI】

操作系统的启动分为两个阶段:引导boot和启动startup,本节主要还是boot过程:

UEFI->GRUB->Linux内核【硬盘引导、UEFI】

1、按开机键,系统加电

2、主板通电

3、UEFI开始执行【UEFI功能比BIOS强大很多,支持命令行,有简单图形界面,也支持文件系统】
3.1、UEFI会检测硬件,并对设备执行简单的初始化工作
3.2、UEFI会判断启动模式,是UEFI还是Legacy【Legacy模式下,UEFI通过CSM模块支持MBR方式启动】
3.3、如果是UEFI模式启动,UEFI会读取硬盘分区表,查找并挂载ESP分区【 EFI System Partition,VFAT格式】
GPT分区下有特殊GUID: C12A7328-F81F-11D2-BA4B-00A0C93EC93B;
MBR分区下有 标识为 0xEF
3.4、各操作系统引的导程序按规则存放到/boot/efi目录下【可以操作文件而不需操作扇区,文件大小限制也宽松了很多】
比如Ubuntu,/boot/efi/ubuntu/grubx64.efi
【可以先引导grub,然后引导Linux】
【也可以直接启动系统内核,包括Windows和Linux,但他们也都需要一个efi文件用于引导系统】

4、UEFI加载efi文件并启动
如果用grub,EFI boot manager会加载/EFI/ubuntu/boot/grubx64.efi,移交控制权,会进入到grub2阶段【grub.cfg也在这个目录下】
如果用ubuntu,EFI boot manager会加载/EFI/ubuntu/boot/ubuntu.efi,移交控制权,可以直接启动linux内核【编译时打开EFI Boot Stub】
如果用windows,EFI boot manager会加载/EFI/Mirosoft/Boot/bootmgr.efi
如果采用默认启动,会使用/EFI/Boot/bootx64.efi

5、后续过程,和BIOS流程就比较相似了

Linux启动过程01【BIOS】

操作系统的启动分为两个阶段:引导boot和启动startup,本文主要描述boot过程。

BIOS->GRUG1->GRUB1.5->GRUB2->Linux内核【环境硬盘引导、MBR分区】

1、按开机键,系统加电

2、主板通电
CPU加电时,会默认设置[CS:IP]为[0XF000:0XFFF0],根据实模式下寻址规则,CPU指向0XFFFF0
这个地址正是BIOS启动程序位置,而BIOS访问方式与内存一致,所以CPU可以直接读取命令并执行

3、BIOS执行
3.1、BIOS首先执行POST自检,包括主板、内存、外设等,遇到问题则报警并停止引导

3.2、BIOS对设备执行简单的初始化工作

3.3、BIOS 会在内存中:
建立中断表(0x00000~0x003FF)
构建 BIOS 数据区(0x00400~0x004FF)
加载了中断服务程序(0x0e05b~0x1005A)

3.4、BIOS根据设备启动顺序,依次判断是否可以启动
比如先检查光驱能否启动
然后依次检查硬盘是否可以启动【硬盘分区的时候,设置为活动分区】

4、硬盘引导
4.1、先说下寻址方式,与扇区编号的事情
最传统的磁盘寻址方式为CHS,由三个参数决定读取哪个扇区:磁头(Heads)、柱面(Cylinder)、扇区(Sector)
磁头数【8位】,从0开始,最大255【微软DOS系统,只能用255个】,决定了读取哪个盘片的哪个面【一盘两面】
柱面数【10位】,从0开始,最大1023【决定了读取哪个磁道,磁道无论长短都会划分为相同扇区数】
扇区数【6位】,从1开始,最大数 63【CHS中扇区从1开始,而逻辑划分中扇区从0开始,经常会造成很多误解】
每个扇区为512字节

4.2、然后说下引导方式
BIOS在发现硬盘启动标志后,BIOS会引发INT 19H中断
这个操作,会将MBR【逻辑0扇区】,也就是磁盘CHS【磁头0,柱面0,扇区1】,读取到内存[0:7C00h],然后执行其代码【GRUB1阶段】,至此BIOS把主动权交给了GRUB1阶段代码
MBR扇区为512字节,扇区最后分区表至少需要66字节【64字节DPT+2字节引导标志】,所以这段代码最多只能有446字节,grub中对应的就是引导镜像boot.img
boot.img的任务就是,定位,并通过BIOS INT13中断读取1.5阶段代码,并运行

5、Grub1.5阶段
5.1、先说一下MBR GAP
据说微软DOS系统原因,第一个分区的起始逻辑扇区是63扇区,在MBR【0扇区】和分布表之间【63扇区】,存在62个空白扇区,共 31KB。
Grub1.5阶段代码就安装在这里。

5.2、上面提到,boot.img主要功能就是找到并加载Grub1.5阶段代码,并切换执行。
Grub1.5阶段代码是core.img,其主要功能就是加载文件系统驱动,挂载文件系统, 位加载并运行GRUB2阶段代码。
core.img包括多个映像和模块:
diskboot.img【1.5阶段引导程序】,存在于MBR GAP第一个扇区;【这里是硬盘启动的情况,如果是cd启动就会是cdboot.img】
lzma_decompress.img【解压程序】
kernel.img【grub核心代码】,会【压缩存放】
biosdisk.mod【磁盘驱动】、Part_msdos.mod【MBR分区支持】、Ext2.mod【EXT文件系统】等,会【压缩存放】

其实boot.img只加载了core.img的第一个扇区【存放diskboot.img】,然后控制权就交出去了,grub阶段1代码使命结束。
diskboot.img知道后续每个文件的位置,会继续通过BIOS中断读取扇区,加载余下的部分并转交控制权,包括:
加载lzma_decompress.img,从而可以解压被压缩的模块
加载kernel.img,并转交控制权给kernel.img
kernel.img的grub_main函数会调用grub_load_modules函数加载各个mod模块
加载各个mod后,grub就支持文件系统了,访问磁盘不需要再依靠BIOS的中断以扇区为单位读取了,终于可以使用文件系统了

6、GRUB2阶段
现在grub就能访问boot/grub及其子目录了
kernel.img接着调用grub_load_normal_mode加载normal模块
normal模块读取解析文件grub.cfg,查看有哪些命令,比如发现了linux、initrd这几个命令,需要linux模块
normal模块会根据command.lst,定位并加载用到的linux模块【一般在/boot/grub2/i386-pc目录】
当然,同时需要完成初始化显示、载入字体等工作
接下来Grub就会给咱们展示启动菜单了

7、选择启动菜单
7.1、引导协议
引导程序加载内核,前提是确定好数据交换方式,叫做引导协议,内核中引导协议相关部分的代码在arch/x86/boot/header.S中,内核会在这个文件中标明自己的对齐要求、是否可以重定位以及希望的加载地址等信息。同时也会预留空位,由引导加载程序在加载内核时填充,比如initramfs的加载位置和大小等信息。
引导加载程序和内核均为此定义了一个结构体linux_kernel_params,称为引导参数,用于参数设定。Grub会在把控制权移交给内核之前,填充好linux_kernel_params结构体。如果用户要通过grub向内核传递启动参数,即grub.cfg中linux后面的命令行参数。Grub也会把这部分信息关联到引导参数结构体中。

#结构体对照
#grub源码
linux_i386_kernel_header
linux_kernel_params

#linux源码
arch/x86/boot/header.S
arch/x86/boot/boot_params.h    boot_params
arch/x86/boot/boot_params.h    setup_header

7.2、开始引导
Linux内核的相关文件位于/boot 目录下,文件名均带有前缀 vmlinuz。
咱们选择对应的菜单后,Grub会开始执行对应命令,定位、加载、初始化内核,并移交到内核继续执行。
调用linux模块中的linux命令,加载linux内核
调用linux模块中的initrd命令,填充initramfs信息,然后Grub会把控制权移交给内核。
内核此时开始执行,同时也就可以读取linux_kernel_params结构体的数据了
boot阶段结束,开始进入startup阶段。

记一次分布式锁引发的生产问题

近期发版,要修复一个并发引起的主键冲突报警。

运维要求没有此类报警,组里同事顺手就改了。

结果第二天一早服务高峰期,服务直接卡死没有反应了。

快速定位到是这段代码的问题,回滚,解决问题。

然后分析了一下,开发、测试、架构都有问题:

原始逻辑:
1、服务A加了分布式锁L1
2、服务A调用服务B
3、服务B加了分布式锁L2
4、服务B调用数据库服务入库
由于加锁逻辑比较特殊,并发时,会造成主键冲突。

问题逻辑:
1、服务A加了分布式锁L1
2、服务A调用服务B
3、服务B加了分布式锁L1
4、服务B调用数据库服务入库
服务直接卡死,要么服务超时,要么锁超时,服务能不卡死吗

解决问题并不复杂,在这里就不罗嗦了。

问题是很简单,但暴漏的问题十分多:
1、开发对程序逻辑、分布式锁理解明显不够
2、在开发、测试、UAT环境下,开发和测试其实都发现了性能下降的问题,但都没有重视
3、架构和资深开发,代码审核工作问题也很大
其实很多时候,工具是一回事,但人员的专业性思维,其实更重要。

记一次HTTPS授权失败

我们伟大的云提供商告诉我们,他们廊坊机房要关停了,要求我们迁移到其他机房,春节前一定要迁移完毕。

和运维、架构沟通后,做了迁移计划,做了演练,虽然时间仓促,问题特别多,但还是扛过去了。

迁移后,不少用户受到了影响,比如有一个省由于网络供应商问题,整个省的用户无法解析DNS。

这其中,印象最深的是,我们一个用户,服务、配置、权限各种都是正确的。但就是无法登录。

分析了几个小时,最后发现,原来是用户机器时间不对,时差有几个小时,在HTTPS请求到达防火墙时,就被干掉了。

设好时间,一切正常。

其实,之前也遇到过两三次类似问题,频率太低了,一开始根本就没考虑这种可能。

一次MacOS升级引发的灾难

一、上周帮老婆大人把MacOS从Mojave升级到了Catalina,然后悲剧发生了。
任何需要截屏、屏幕共享或屏幕录制的应用都不能使用了,具体表现为:
1、配置-》安全与隐私里,多了一个选项”屏幕录制/Screen Recording”,里面列表是空的。
2、快捷键录屏是可以的
3、打开QuickPlayer,录制屏幕,提示之前禁用过屏幕录制,要求去修改权限;但由于列表是空的,根本无法做任何操作
4、其他APP,如微信、腾讯会议什么的,提示要增加权限,但“屏幕录制”里还是空的。

二、网上找了一下,无论国内国外,都有一些用户遇到了这个问题。尝试了一些建议的方法,搞不定。

#正常模式下,显示失败
sudo tccutil reset ScreenCapture

#恢复模式下,显示成功,重启后无效
tccutil reset ScreenCapture

三、于是打电话给Apple,一位妹子远程视频帮忙处理了很久,把能重置配置的方式都用了,还是不行。最后建议我重装系统。

四、重装,没任何变化。

五、把系统升级到了Big Sur,好歹有了一点儿进展:
1、配置-》安全与隐私里,”屏幕录制/Screen Recording”,里面列表是空的,但是新增任何APP都无效
2、快捷键录屏是可以的
3、QuickPlayer录制屏幕是可以的
4、其他APP,如微信、腾讯会议什么的,提示要增加权限,但“屏幕录制”里还是空的。

六、找了好长时间,最后终于解决了,步骤如下:
1、重启系统,听到提示音时,按下Command+R,进入恢复模式
2、恢复模式下,开启Terminal,禁用SIP服务 (system integrity protection)

csrutil disable

3、重启系统,进入正常模式,重命名tcc.db文件

sudo mv /Library/Application\ Support/com.apple.TCC/TCC.db /Library/Application\ Support/com.apple.TCC/TCC.db.bak

4、重启系统,进入恢复模式
5、恢复模式下,开启Terminal,启用SIP服务 (system integrity protection)

csrutil enable

6、重启系统,进入正常模式
7、你会发现问题解决了

整体来说,这应该是MacOS升级的一个Bug,出现频率并不高,而且横跨两个大版本至今没有解决。
(也可以这么说,Big Sur解决问题的方式只是把QuickTime加白名单了,根本没有解决根本问题;或者说解决问题的Apple程序员可能根本没有定位到问题产生的原因)

我自己解决这个问题花费了4个小时以上,如果没有编程经验的普通用户遇到,估计只能格式化重装了,希望Apple可以出个工具,解决此类问题,节约大家的时间。

记一次Excutor引发的生产故障

当时看到《阿里巴巴Java编码规范》时,我感觉这里面说的不是大家都该知道的事情吗?真有必要汇总个规范出来?

出来混,总要还的,最近在老项目上就遇到了。

我们有两个服务,遗留项目,代码写的实在是不敢恭维。
用量其实不大,或者说偏小,但很默契的每两周挂一次,到今天是第三次了。

第一次OOM(没定位到根本问题):
K8S网络组件异常,DNS解析失败,和架构一起排查后,发现问题如下:
1、系统原来使用过RabbitMQ,后来代码注释了
2、但POM中,还是有MQ的引用,别的不会做,但会启动MQ的监听
3、结果有一天K8S网络组件异常,DNS解析失败
4、MQ的心跳包瞬间起了几千个线程,连接找不到的MQ Server
5、服务挂了

推断的问题链路为:
DNS无法解析-》MQ心跳包不断起线程,而且没有连接池-》线程耗尽内存-》OOM-》服务挂掉

解决方法:
在POM中,干掉了MQ的引用,以为问题被修复了。

第二次OOM(大意了):
1、表现仍是K8S网络组件异常,DNS解析失败
2、架构感觉容器DNS解析不了应该运维去查
3、只好拉上运维查,运维也比较给力,很快把日志给出来了
4、但是系统OOM时,并没有生成镜像,架构建议添加JVM参数,结果和运维排查后发现JVM参数已经加了,OOM应该有Dump啊
5、最后的结论是,非常规问题,等下次发生再解决
6、这个时候,已经感觉怪怪的了,问题根源根本没有找到啊,但大家都忙着机房搬迁,就没能跟下去(我的问题)

推断的问题链路为:
DNS无法解析-》不断起新线程去连接-》线程耗尽内存-》OOM-》但没有Dump文件-》无法定位问题

解决方法:
碰运气,等下一次OOM生成Dump文件?我当时咋想的呢

第三次OOM(真的解决问题了吗?):
1、感觉不对劲啊,为什么总是两周挂,于是拉取了日志
2、日志上没看出什么来
3、仍然没有OOM的Dump
4、jstack一看线程,傻眼了,一堆线程在waiting on condition
5、赶快看相关源码,居然是用了Excutor,然后线程和队列都是默认的
6、你懂的,线程数和队列默认几乎都是无限的,参数设置错误,导致线程池根本不会服用原来的工作线程
7、服务挂掉是早晚的事情

推断的问题链路为:
DNS无法解析-》不断起新线程去连接-》线程耗尽内存-》OOM-》但没有Dump文件-》无法定位问题

解决方法:
修改Executor默认配置

咋说呢,再次认识到规范强制推行也是必要的,全靠人的个人水平,是不行的。他们的规范,恐怕也是各种坑汇总成的结晶吧。

记一次PG引发的生产故障

前几天,突然收到报修,一个文件接收服务突然宕了。
我们运维同事很快就进行了重启,此时怪异的事情发生了:
重启服务后,服务一会儿就没响应了,端口可以通,文件全部上传失败,没有任何异常日志输出。

那就排查吧:
1、PG数据库连接正常,无死锁,表空间正常,数据查询秒回
2、服务配置几个月都没人改过,CPU、内存、存储、IO、网络,全部正常
3、上传客户端日志输出,显示其工作正常
4、只有文件接收服务,没有响应,也没有日志输出
初步判断,文件接收服务出问题了。

于是,新开一个云主机,重新安装服务,仍然无响应。
然后,拷贝了一个正常工作的主机,修改配置后,仍然无响应。
来来回回折腾了几个小时,还是不行。

无奈之余,试了一下向PG数据库插入数据,我去,几十秒都没有响应。
赶快问DBA,原来是做了高可用,主从库数据同步,从库异常了,导致主库可读不可写。
众人皆表示无奈,重启从库,问题解决。

其实,一开始就排查数据库了,但由于是生产库,只试过查询,没有试过写入。
但是,我们都大意了:
1、服务日志,输出不够详细,就算DEBUG打开,也不知道数据进行到哪一步了,这个最坑
2、没有正确的设置超时时间,原因是接收文件后,写入数据库一直在等待,服务日志没有任何数据库异常
3、数据库监视软件,只做了主库监视,根本没做从库监视
4、数据库主从配置,本应该是异步,但现在配置成了同步
5、没有监视主从库同步的情况
6、生产库,不敢轻易进行写操作,只看了查询效率及死锁,没有看慢语句

就这样一个小问题,绕过了层层监控机制,让问题排查十分困难,花费了大量的人力。
反思起来,如果只是为了记录日志而记录日志,但日志不能反应服务状态,那不如不记;如果只是为了监控而监控,但监控不到位,那不如不监控。
我们日常做事情,也是同样的道理,细节是魔鬼,把该把控的把控好了,才能提升效率,得到想要的结果。

快速提升单元覆盖率

最近到新公司,接手了几十个老项目。由于项目特殊需要,需要快速将一个模块的单元测试覆盖率提升到80%以上。

怀着忐忑的心情看了一下,该模块居然还有一个单元测试,整体覆盖率为0,欲哭无泪啊。

手工写是来不及了,那就想办法自动生成吧。找了一下,最终决定采用EvoSuite。

EvoSuite有多种方式可以配置,包括命令行模式、Maven插件模式以、Eclipse插件模式、IDEA插件模式等。

一、maven模式
1、修改POM文件,在对应位置添加相关内容

<properties>
	<evosuiteVersion>1.0.6</evosuiteVersion>
</properties>

<dependencies>
	<dependency>
		<groupId>junit</groupId>
		<artifactId>junit</artifactId>
		<version>4.12</version>
		<scope>test</scope>
	</dependency>
	<dependency>
		<groupId>org.evosuite</groupId>
		<artifactId>evosuite-standalone-runtime</artifactId>
		<version>${evosuiteVersion}</version>
		<scope>test</scope>
	</dependency>
</dependencies>

<build>
	<pluginManagement>
		<plugins>
			<plugin>
				<groupId>org.eclipse.m2e</groupId>
				<artifactId>lifecycle-mapping</artifactId>
				<version>1.0.0</version>
				<configuration>
					<lifecycleMappingMetadata>
						<pluginExecutions>
							<pluginExecution>
								<pluginExecutionFilter>
									<groupId>org.apache.maven.plugins</groupId>
									<artifactId>maven-compiler-plugin</artifactId>
									<versionRange>[2.5,)</versionRange>
									<goals>
										<goal>prepare</goal>
									</goals>
								</pluginExecutionFilter>
								<action>
									<ignore />
								</action>
							</pluginExecution>
						</pluginExecutions>
					</lifecycleMappingMetadata>
				</configuration>
			</plugin>
		</plugins>
	</pluginManagement>
	<plugins>
		<plugin>
			<groupId>org.evosuite.plugins</groupId>
			<artifactId>evosuite-maven-plugin</artifactId>
			<version>${evosuiteVersion}</version>
			<executions>
				<execution>
					<goals>
						<goal>prepare</goal>
					</goals>
					<phase>process-test-classes</phase>
				</execution>
			</executions>
		</plugin>

		<plugin>
			<groupId>org.apache.maven.plugins</groupId>
			<artifactId>maven-surefire-plugin</artifactId>
			<version>2.17</version>
			<configuration>
				<properties>
					<property>
						<name>listener</name>
						<value>org.evosuite.runtime.InitializingListener</value>
					</property>
				</properties>
			</configuration>
		</plugin>

		<!--plugin>
			<groupId>org.codehaus.mojo</groupId>
			<artifactId>build-helper-maven-plugin</artifactId>
			<version>1.8</version>
			<executions>
				<execution>
					<id>add-test-source</id>
					<phase>generate-test-sources</phase>
					<goals>
						<goal>add-test-source</goal>
					</goals>
					<configuration>
						<sources>
							<source>.evosuite/evosuite-tests</source>
						</sources>
					</configuration>
				</execution>
			</executions>
		</plugin-->

		<plugin>
			<artifactId>maven-compiler-plugin</artifactId>
			<version>3.8.1</version>
			<configuration>
				<source>1.8</source>
				<target>1.8</target>
			</configuration>
		</plugin>
	</plugins>
</build>

2、生成单元测试

mvn -DmemoryInMB=4000 -Dcores=4 evosuite:generate test

#生成的单元测试在
#.evosuite/best-tests
#拷贝到正确的路径就可以了

二、命令行模式
1、下载evosuite-1.0.6.jar包

Downloads

2、收集项目依赖,把evosuite-1.0.6.jar也放入target/dependency文件夹

mvn dependency:copy-dependencies

3、生成单元测试

cd target/dependency
java -jar evosuite-1.0.6.jar -help
java -Duse_separate_classloader=false -jar evosuite-1.0.6.jar -projectCP YOUR_CLASS_PATH -generateSuite -target ..\classes

#生成的单元测试在
#target/dependency/evosuite-tests
#拷贝到正确的路径就可以了

三、Eclipse插件模式
在eclipse中安装evosuite插件,需要额外的插件地址:
http://www.evosuite.org/update

四、单元覆盖率
1、插件安装
在eclipse中搜索并安装EclEmma Java Code Coverage插件,直接搜索即可

2、修改class loader配置

#默认使用单独的class loader,覆盖率会为0
separateClassLoader = true
#全局替换为
separateClassLoader = false

3、然后在项目上,右键,Coverage as-》JUnit Test
就可以看到覆盖率了哦。
我试过两个项目,一个简单的项目,覆盖率为95以上。
一个复杂一些的Web项目,覆盖率仅为30%左右。

五、总结
生成的单元测试,实际上没有什么维护性,如何用于生产环境,待探索。