如何解决内存碎片问题


如何解决内存碎片问题?
————NEOHOPE的内存碎片优化手册

其实无论采用哪种分配方式,内存的碎片化都是难以彻底避免的。无论是操作系统、虚拟机还是应用,都要面对这个问题。
业界有多种思路来解决或缓解此问题:

一、操作系统层面
1、把不可移动内存集中管理,内存分区其实在一定程度上解决了这些问题
从操作系统的角度来说,有些内存页面是可以移动的,有些是无法移动的。如果无法移动的页面过于分散,整个系统内存的连续性就很差,碎片化就更严重,而且更加不容易管理。所以在操作系统设计的时候,一般会根据功能用途对内存进行分区,一定程度上避免此类问题。

2、linux采用了buddy system来缓解内存碎片问题
buddy system是一个很巧妙的设计,将内存连续页面按2的n次方进行分组。
申请时会匹配第一个大于等于所需页面的2的n次方连续页面,并将多余页面拆分后挂载到对应的2的x方组。
释放内存页面时,仍按2的n次方进行归还,并尝试将内存进行合并。

3、linux中为了处理特别小的内存请求,引入了slab技术,来辅助buddy system
类似于对象池的概念,系统先申请一部分页面用于对象池。
申请时划分一部分出来给应用使用,如果内存不足会进行主动扩容。
归还时对象还给对象池,如果空闲对象比较多时,会主动释放部分内存。

4、windows有一种LFH(Low Fragmentation Heap)技术,缓解内存碎片问题
在应用程序启动时,操作系统会额外分配一定的连续内存LFH给这个进程备用
如果应用需要使用内存,会优先从LFH中申请,从而降低系统层面的内存管理负担

5、windows在进程退出后,不会立即释放dll文件内存
一方面提速,如果关闭一个应用,再开启,就会感觉很快
另一方面也缓解了操作系统内存管理负担。
其实,看下你手机的APP,切换到后台时,就是这个效果

6、内存整理服务
无论是linux还是windows都有低优先级线程,在后台默默做着内存规整工作,类似于磁盘碎片清理
比如linux内核中的kcompactd线程。

7、类似与LFH,可以考虑在内存分配时额外预留一部分,下次分配在预留的地方继续分配
windows在xp时代,需要应用自行开启LFH功能,但vista之后,操作系统会同一进行管理

8、为了内存规整方便,可以考虑靠近应用已分配内存区域进行分配
其实可操作性不高,还不如上一条可行性好一些

9、还有一种思路,就是将不连续的内存,转换为逻辑上连续的内存,来绕过碎片化问题
但一些情况下性能难以保证

二、虚拟机层面
1、JVM整体内存会被划分为多个部分,类似于做了分区:
一部分是虚拟机共有的【方法区、堆】,一部分是线程私有的【虚拟机栈、本地栈、程序计数器】

2、同时,JVM虚拟机也会根据对象的生命周期,类似进一步做了分区,而且不同分区采用不同GC策略
最常用的就是年代划分法,新生代、老年代、永久代【后来的Metaspace】

3、JVM虚拟机,GC时会通过标记-整理(比如CMS)或复制-清除(比如G1)的方法来解决部分碎片问题

三、应用层面
1、redis在处理内存的时候,申请时会额外申请一部分先备着【记得是jemalloc】

2、redis释放时也不会立即释放,有单独的线程进行处理,在应用层面去降低系统内存管理负担

3、redis在数据结构上也做了很多努力

4、在写程序时,如果需要频繁创建和释放对象,可以尝试使用对象池

5、在写程序的时候,尽量不要零零散散的去申请大量小内存;

6、除了标准库以外,可以试一下 jemalloc或Doug Lea’s malloc

7、感兴趣可以看下redis内存管理的代码

如何写出让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、生产库,不敢轻易进行写操作,只看了查询效率及死锁,没有看慢语句

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