
Category Archives: DevOps
代码跨平台实现方式

WSL2中apt升级systemd时报错:无法锁定passwd文件
1、环境:
Windows10+WSL2+Ubuntu24
PS:另一台电脑Windows11+WSL2+Ubuntu24,不会报错
2、再现方式及错误信息
# apt-get upgrade Reading package lists... Done Building dependency tree... Done Reading state information... Done Calculating upgrade... Done ... ... Setting up systemd (255.4-1ubuntu8.8) ... Initializing machine ID from random generator. Failed to take /etc/passwd lock: Invalid argument dpkg: error processing package systemd (--configure): installed systemd package post-installation script subprocess returned error exit status 1 Errors were encountered while processing: systemd E: Sub-process /usr/bin/dpkg returned an error code (1)
3、错误发生原因
systemd升级的脚本,会调用systemd-sysusers,systemd-sysusers会尝试通过fcntl锁定文件,但WSL中fcntl实现效果与Linux中不同,导致脚本执行失败。
更进一步的解释:
Linux中文件锁是基于文件描述符的,子进程会自动继承该文件锁。
Windows中文件锁是基于进程的,子进程需要自行获取新的文件锁。
WSL中,实现方式,更接近与Windows,重复获取同一个文件的锁自然是失败的。
openat(AT_FDCWD, "/etc/.pwd.lock", O_WRONLY|O_CREAT|O_NOCTTY|O_NOFOLLOW|O_CLOEXEC, 0600) = 3
fcntl(3, F_OFD_SETLKW, {l_type=F_WRLCK, l_whence=SEEK_SET, l_start=0, l_len=0}) = -1 EINVAL (Invalid argument)
4、如何绕过该错误
# 原文在此:https://github.com/microsoft/WSL/issues/10397
# 切换到/bin
# 将systemd-sysusers修改为systemd-sysusers.org
# 将systemd-sysusers做成一个echo的符号链接(用于欺骗升级脚本,让其以为得到了正确的结果)
# 切换回之前的目录
cd /bin && mv -f systemd-sysusers{,.org} && ln -s echo systemd-sysusers && cd -
# 修复包依赖
apt --fix-broken install
# 继续升级
apt-get upgrade
记一次存储Inode数量引发的生产故障
前一段时间,突然收到了系统报警,某上传服务异常。
经过排查,上传服务正常,但存储无法正常写入,一直写入失败,表现为:
1、一块新盘,32T,已使用2T,可用30T,控制台和命令行操作结果一直
2、服务写入时,一直报“no space left on device”
3、没有收到任何存储报警
立刻找了云服务厂商的老师,解决了问题:
1、除了限制写入文件总量的大小、并发写入的速度,同时还限制了inode数量
2、上传服务,写入了大量小文件,耗尽了inode数量
3、上传服务,再次写入后,inode申请失败,导致写入失败
4、存储组的老师,紧急扩展了inode数量,解决了问题
经排查,云服务商反馈:
1、为了控制成本,我们之前买了一块较小的硬盘,然后进行了扩容
2、而存储的底层协议为FlexGroup
3、而FlexGroup的普通卷,在扩容的时候,只要超过了1T,默认的Inode数量就一直为21251126,不再提升
4、而我们的上传服务,一个小文件只有几百k,很快就把Inode数量耗尽了
5、对于Inode数量限制,云服务商没有提供任何监控
虽然FlexGroup的超大卷默认会提升Inode数量,但我们一开始购买的服务确是普通卷,然后进行扩容,扩容后仍是普通卷,就触发了Inode数量不会自动增加这个问题。
后续,我们做了两个约定:
1、尽量采购超大卷
2、如果要采购普通卷,同时提单,增加Inode数量
3、云服务商同步进行产品更新,后续产品迭代时,从根源上解决这个问题
PS:
最近发现,他们居然做了一个inode扩容的功能,默认是最小值,可以手工扩展,也能设置为自动扩展。
不知道是谁定的需求,默认选项不应该是自动扩展吗?
导致惨重代价的运维事故02
OVH数据中心失火
2021年3月10日,欧洲云计算巨头OVH位于法国斯特拉斯堡的机房近日发生严重火灾,该区域总共有4个数据中心。
火灾持续6个小时才被扑灭,发生起火的SBG2数据中心被完全烧毁,共360万个网站下线。
Salesforce全球大宕机
2021年5月11日,Salesforce的服务宕机,持续5个小时。
事故原因,官方反馈为“工程师尝试通过脚本变更DNS配置,但脚本执行超时失败。但不幸的是,这个脚本一直在各节点扩散,最终导致全线崩溃”
访问量激增,导致各类一码通服务宕机
2021年12月20日,西安“一码通”崩溃。
2022年1月4日,西安“一码通”崩溃。
2022年1月10日,广州“粤康码”崩溃。
2022年3月11日,上海“随申码”崩溃。
Facebook严重宕机
2021年10月5日,Facebook旗下应用出现大面积故障,波及到Facebook、Messenger、Instagram和WhatsApp等几乎所有产品。
此次宕机长达7个小时,影响到全球数十个国家和地区的几十亿用户。
官方给出的事故原因为:“协调数据中心之间网络流量的主干路由器的配置变化导致了通信中断,由此对我们数据中心的通信方式产生了连带影响,使我们的服务陷入停顿”
小插曲:
Facebook股价盘中暴跌6%,市值减少数百亿美元,扎克伯格个人财富一日蒸发逾60亿美元。
宕机期间,大量用户涌向了Twitter、Telegram等其他应用,又进一步导致这些应用程序的服务器崩溃。
B站713事故
2022年7月13日,B站崩了5个小时。
根据B站的事故分析报告,是SLB故障导致
本次SLB故障,是OpenResty中,计算gcd的lua代码传入了0值,被lua判定为nan,陷入了死循环
这段lua代码已经稳定运行了一段事件,但一个新发布模式,却触发了这个bug
Twitter事故
2022年7月14日,Twitter崩了1个小时。
小插曲:
其实对于Twitter的崩溃,大家都已经习惯了,每年不崩溃几次,都感觉不正常。
阿里云香港机房重大事故
2022年12月18日,由于香港可用区C机房冷却系统失效,现场包间温度逐渐升高,导致一机房包间温度达到临界值触发消防系统喷淋,电源柜和多列机柜进水,部分机器硬件损坏,整个处置过程超过15小时。
事故后,阿里云总裁、CTO都被更换。
唯品会机房宕机事故
2023年3月29日凌晨,唯品会南沙IDC冷冻系统故障,导致机房设备温度快速升高宕机,造成线上商城停止服务
宕机事件达到12小时,损失超亿元,影响客户达800万,唯品会将此次故障判定为P0级故障。
事故后,基础平台部门负责人被免职。
腾讯云机房事故
23年3月29日凌晨,腾讯云广州五区部分云服务异常,导致微信、QQ、支付等核心功能受到影响,故障在当天中午基本恢复。
事故原因,官方反馈为“本次事故由广州电信机房冷却系统故障导致”。
微软Azure故障
2023年5月24日,微软Azure DevOps在巴西的一处scale-unit发生故障,导致宕机约10.5个小时。
导致该中断的原因为一个简单的拼写错误,最终导致17个生产级数据库被删除。
语雀重大服务故障
2023年10月23日,语雀出现重大服务故障,持续7个多小时才完全恢复。
故障原因为新的运维升级工具bug,导致华东地区生产环境存储服务器被误下线,造成大面积服务中断。
阿里云全球故障
2023年11月12日,阿里云爆发全球故障,阿里产品全线崩溃,几乎影响全部云用户,持续事件达3.5小时。
故障原因,具说是鉴权服务出了问题。
小插曲:
两周后,在2023年11月27日,阿里云再次遭遇了近两小时的中断,影响到中国和美国的客户。
然后当天晚上,滴滴就来了个大的。
滴滴故障
2023年11月27日晚间,滴滴崩溃,APP功能无法使用,直至第二天才陆续恢复,故障事件超过12小时。
故障原因,官方反馈说是“底层系统软件发生故障”。
腾讯云崩溃事件
24年4月8日,由于API服务新版本向前兼容性考虑不够和配置数据灰度机制不足,导致1957个客户报障,故障持续90分钟
微软蓝屏事件
24年7月19日,使用了Windows操作系统的设备大面积蓝屏,导致850万设备受到影响。
故障原因是,微软安全供应商CrowdStrike推送了错误的软件配置。
阿里云新加坡机房火灾
24年9月10日上午,阿里云新加坡可用区C数据中心发生火灾,导致主要科技公司服务中断。
火灾原因已确定为锂电池爆炸,持续36小时以上。
此故障,影响到了TikTok、Lazada等多家用户。
============================================================
注:本文主要是整理了运维导致的惨痛代价,并没有记录下面几种情况(设计失败,黑客攻击,病毒爆发)
Linux5.9源码
最近阅读了部分Linux5.9源码,添加了一些注释,感兴趣的同学可以看下。
https://github.com/neohope/NeoLinux/
Linux虚拟化管理
1、内核模块初始化
module_init(vmx_init)->kvm_init module_init(svm_init)->kvm_init //其中,kvm_init ->kvm_arch_init ->kvm_irqfd_init ->kvm_arch_hardware_setup ->misc_register(&kvm_dev)
2、从数据结构角度,又可以看到了设备皆为文件的思想
static struct miscdevice kvm_dev = {
KVM_MINOR,
"kvm",
&kvm_chardev_ops,
};
static struct file_operations kvm_chardev_ops = {
.unlocked_ioctl = kvm_dev_ioctl,
.llseek = noop_llseek,
KVM_COMPAT(kvm_dev_ioctl),
};
//初始化时,通过misc_register,实现了操作的绑定。
3、通过上面的数据结构,我们就可以找到创建虚拟机的方法,并生成控制文件
kvm_dev.kvm_chardev_ops.kvm_dev_ioctl
或者,ioctl系统调用KVM_CREATE_VM,效果也是一样的:
SYSCALL_DEFINE3(ioctl, unsigned int, fd, unsigned int, cmd, unsigned long, arg)
->vfs_ioctl,会用到vfs_ioctl.unlocked_ioctl也就是kvm_dev_ioctl
->case KVM_CREATE_VM:
-> r = kvm_dev_ioctl_create_vm(arg);
->file = anon_inode_getfile("kvm-vm", &kvm_vm_fops, kvm, O_RDWR);
//其中,kvm_dev_ioctl_create_vm
->kvm_create_vm
->->kvm_arch_init_vm
->->hardware_enable_all
->->kvm_arch_post_init_vm
->->list_add(&kvm->vm_list, &vm_list);
4、生成虚拟CPU套路很相似,仍是文件操作
static struct file_operations kvm_vm_fops = {
.release = kvm_vm_release,
.unlocked_ioctl = kvm_vm_ioctl,
.llseek = noop_llseek,
KVM_COMPAT(kvm_vm_compat_ioctl),
};
//创建虚拟机时,通过anon_inode_getfile,实际上就把文件和kvm_vm_fops绑定了起来。
anon_inode_getfile("kvm-vm", &kvm_vm_fops, kvm, O_RDWR)
5、在调用ioctl时
SYSCALL_DEFINE3(ioctl, unsigned int, fd, unsigned int, cmd, unsigned long, arg) ->vfs_ioctl,会用到vfs_ioctl.unlocked_ioctl也就是kvm_vm_ioctl kvm_vm_ioctl->kvm_vm_ioctl_create_vcpu ->kvm_arch_vcpu_precreate ->kvm_vcpu_init ->kvm_arch_vcpu_create ->kvm_get_kvm ->create_vcpu_fd,生成设备文件inode ->kvm_arch_vcpu_postcreate //其中,kvm_arch_vcpu_create ->kvm_mmu_create ->vcpu->arch.user_fpu = kmem_cache_zalloc(x86_fpu_cache, GFP_KERNEL_ACCOUNT); ->kvm_pmu_init(vcpu); ->kvm_hv_vcpu_init(vcpu); ->kvm_x86_ops.vcpu_create(vcpu); ->kvm_vcpu_mtrr_init(vcpu); ->vcpu_load(vcpu); ->kvm_vcpu_reset(vcpu, false); ->kvm_init_mmu(vcpu, false); //包括init_kvm_tdp_mmu和init_kvm_softmmu两种虚拟化方式
6、启动虚拟机,还是文件操作
static struct file_operations kvm_vcpu_fops = {
.release = kvm_vcpu_release,
.unlocked_ioctl = kvm_vcpu_ioctl,
.mmap = kvm_vcpu_mmap,
.llseek = noop_llseek,
KVM_COMPAT(kvm_vcpu_compat_ioctl),
};
7、在调用ioctl时KVM_RUN
SYSCALL_DEFINE3(ioctl, unsigned int, fd, unsigned int, cmd, unsigned long, arg) ->vfs_ioctl,会用到vfs_ioctl.unlocked_ioctl也就是kvm_vcpu_ioctl kvm_vcpu_ioctl-> case KVM_RUN: kvm_arch_vcpu_ioctl_run //其中, kvm_arch_vcpu_ioctl_run->vcpu_run->vcpu_enter_guest
8、IO同样有虚拟化和半虚拟化两种
一个处理函数为kvm_fast_pio,另一个为kvm_emulate_instruction
Linux系统调用03
Linux系统调用的整体流程为:
1、应用程序【用户态】通过syscall或glibc进行内核功能调用,这一部分在glibc源码中进行的
2、CPU收到syscall,Linux内核响应syscall调用【内核态】,这一部分在linux源码中进行的
3、返回结果到应用程序【用户态】
本节,给Linux系统,增加一个新系统调用功能,获取cpu数量。
1、新建一个源码编译目录
mkdir kernelbuild
2、下载源码,解压
wget https://mirrors.edge.kernel.org/pub/linux/kernel/v5.x/linux-5.10.59.tar.gz tar -xzf linux-5.10.59.tar.gz cd linux-5.10.59
3、清理
make mrproper
4、修改文件
4.1、arch/x86/entry/syscalls/syscall_64.tbl
#在440后面增加一行
441 common get_cpus sys_get_cpus
4.2、include/linux/syscalls.h
#在最后一个asmlinkage增加一行
asmlinkage long sys_get_cpus(void);
4.3、kernel/sys.c
#在最后一个SYSCALL_DEFINE0后面增加下面几行
//获取系统中有多少CPU
SYSCALL_DEFINE0(get_cpus)
{
return num_present_cpus();
}
5、内核配置
make menuconfig make oldconfig
6、修改.config,去掉一个证书
CONFIG_SYSTEM_TRUSTED_KEYS=“”
7、编译
make -j4
8、安装
sudo make modules_install sudo make install
9、测试
9.1、新建文件cpus.c
#include <stdio.h>
#include <unistd.h>
#include <sys/syscall.h>
int main(int argc, char const *argv[])
{
//syscall就是根据系统调用号调用相应的系统调用
long cpus = syscall(441);
printf("cpu num is:%d\n", cpus);//输出结果
return 0;
}
9.2、编译
gcc main.c -o cpus
9.3、运行
./cpus 在没有修改的内核上返回是-1 在修改过的为num_present_cpus数量
Linux系统调用02
Linux系统调用的整体流程为:
1、应用程序【用户态】通过syscall或glibc进行内核功能调用,这一部分在glibc源码中进行的
2、CPU收到syscall,Linux内核响应syscall调用【内核态】,这一部分在linux源码中进行的
3、返回结果到应用程序【用户态】
本节处理第二部分:
二、linux内核部分
1、在make时,会通过syscall_64.tbl生成syscalls_64.h,然后包含到syscall_64.c,进行调用号与函数之间的绑定。 arch/x86/entry/syscalls/syscall_64.tbl arch/x86/include/generated/asm/syscalls_64.h arch/x86/entry/syscall_64.c
1.1、以sys_openat为例,在syscall_64.tbl中为
257 common openat sys_openat 441 common get_cpus sys_get_cpus
1.2、make后,在生成的syscalls_64.h中为
__SYSCALL_COMMON(257, sys_openat)
1.3 在syscall_64.c中,展开__SYSCALL_COMMON
#define __SYSCALL_COMMON(nr, sym) __SYSCALL_64(nr, sym) //展开就是 __SYSCALL_64(257, sys_openat)
1.4、在syscall_64.c中,第一次展开__SYSCALL_64
#define __SYSCALL_64(nr, sym) extern long __x64_##sym(const struct pt_regs *); #include <asm/syscalls_64.h> #undef __SYSCALL_64 //展开就是 extern long __x64_sys_openat(const struct pt_regs *); //也就是每个__SYSCALL_64都展开成了一个外部函数
1.5、在syscall_64.c中,第二次展开__SYSCALL_64
#define __SYSCALL_64(nr, sym) [nr] = __x64_##sym,
asmlinkage const sys_call_ptr_t sys_call_table[__NR_syscall_max+1] = {
[0 ... __NR_syscall_max] = &__x64_sys_ni_syscall,
#include <asm/syscalls_64.h>
};
//展开其实就是指向了外部函数
[257]=__x64_sys_openat,
//全部展开结果,都会被包含到sys_call_table中,从而完成了调用号与函数之间的绑定。
2、当产生系统调用时
2.1、应用直接syscall或通过glibc产生了syscall
2.2、cpu会产生类似于中断的效果,开始到entry_SYSCALL_64执行
//文件路径arch/x86/entry/entry_64.S SYM_CODE_START(entry_SYSCALL_64) //省略代码 call do_syscall_64 SYM_CODE_END(entry_SYSCALL_64) //文件路径arch/x86/entry/entry_64.S,32位兼容模式,过程与64位类似 SYM_CODE_START(entry_SYSCALL_compat) call do_fast_syscall_32 SYM_CODE_END(entry_SYSCALL_compat)
2.3、调用do_syscall_64
#ifdef CONFIG_X86_64
__visible noinstr void do_syscall_64(unsigned long nr, struct pt_regs *regs)
{
nr = syscall_enter_from_user_mode(regs, nr);
instrumentation_begin();
if (likely(nr < NR_syscalls)) {
nr = array_index_nospec(nr, NR_syscalls);
regs->ax = sys_call_table[nr](regs);
}
instrumentation_end();
syscall_exit_to_user_mode(regs);
}
#endif
2.4、根据sys_call_table调用对应的功能函数
sys_call_table[nr](regs) 如果我们传入257,就会调用__x64_sys_openat 如果我们传入441,就会调用__x64_sys_get_cpus
2.5、但咱们实际写的函数sys_get_cpus,好像和实际调用函数__x64_sys_get_cpus,差了一个__x64,这需要一个wrapper
arch\x86\include\asm\syscall_wrapper.h
#define SYSCALL_DEFINE0(sname) \
SYSCALL_METADATA(_##sname, 0); \
static long __do_sys_##sname(const struct pt_regs *__unused); \
__X64_SYS_STUB0(sname) \
__IA32_SYS_STUB0(sname) \
static long __do_sys_##sname(const struct pt_regs *__unused)
#define __X64_SYS_STUB0(name) \
__SYS_STUB0(x64, sys_##name)
#define __SYS_STUB0(abi, name) \
long __##abi##_##name(const struct pt_regs *regs); \
ALLOW_ERROR_INJECTION(__##abi##_##name, ERRNO); \
long __##abi##_##name(const struct pt_regs *regs) \
__alias(__do_##name);
SYSCALL_DEFINE0(get_cpus),会展开成为
__X64_SYS_STUB0(get_cpus)
//然后
__SYS_STUB0(x64, sys_get_cpus)
//然后
long __x64_sys_get_cpus(const struct pt_regs *regs);
这样前后就对上了,glibc和linux内核就通了。
Linux系统调用01
Linux系统调用的整体流程为:
1、应用程序【用户态】通过syscall或glibc进行内核功能调用,这一部分在glibc源码中进行的
2、CPU收到syscall,Linux内核响应syscall调用【内核态】,这一部分在linux源码中进行的
3、返回结果到应用程序【用户态】
本节先处理第一部分:
一、glibc部分
1、应用程序调用open函数
//glibc/intl/loadmsgcat.c # define open(name, flags) __open_nocancel (name, flags)
2、展开后实际上调用了
__open_nocancel(name, flags)
3、而__open_nocancel 最终调用了INLINE_SYSCALL_CALL
//glibc/sysdeps/unix/sysv/linux/open_nocancel.c __open_nocancel(name, flags) ->return INLINE_SYSCALL_CALL (openat, AT_FDCWD, file, oflag, mode);
4、宏展开【理解就好,不保证顺序】
4.1、初始为
INLINE_SYSCALL_CALL (openat, AT_FDCWD, file, oflag, mode); 4.2、第1次展开INLINE_SYSCALL_CALL [code lang="c"] #define INLINE_SYSCALL_CALL(...) \ __INLINE_SYSCALL_DISP (__INLINE_SYSCALL, __VA_ARGS__) //展开得到: __INLINE_SYSCALL_DISP(__INLINE_SYSCALL, __VA_ARGS__【openat, AT_FDCWD, file, oflag, mode】) 4.3、第2次展开__INLINE_SYSCALL_DISP [code lang="c"] #define __INLINE_SYSCALL_DISP(b,...) \ __SYSCALL_CONCAT (b,__INLINE_SYSCALL_NARGS(__VA_ARGS__))(__VA_ARGS__) //展开得到: __SYSCALL_CONCAT(b【__INLINE_SYSCALL】,__INLINE_SYSCALL_NARGS(__VA_ARGS__【openat, AT_FDCWD, file, oflag, mode】))(__VA_ARGS__【openat, AT_FDCWD, file, oflag, mode】)
4.4、第3次展开__INLINE_SYSCALL_NARGS
__INLINE_SYSCALL_NARGS(__VA_ARGS__【openat, AT_FDCWD, file, oflag, mode】) #define __INLINE_SYSCALL_NARGS(...) \ __INLINE_SYSCALL_NARGS_X (__VA_ARGS__,7,6,5,4,3,2,1,0,) //展开得到: __INLINE_SYSCALL_NARGS_X(openat, AT_FDCWD, file, oflag, mode,7,6,5,4,3,2,1,0,) //然后展开__INLINE_SYSCALL_NARGS_X #define __INLINE_SYSCALL_NARGS_X(a,b,c,d,e,f,g,h,n,...) n //展开得到参数个数: 4 //从而4.4的结果为 __SYSCALL_CONCAT(__INLINE_SYSCALL,4)(__VA_ARGS__【openat, AT_FDCWD, file, oflag, mode】)
4.5、然后展开__SYSCALL_CONCAT,其实就是字符拼接
__SYSCALL_CONCAT(__INLINE_SYSCALL,4) #define __SYSCALL_CONCAT_X(a,b) a##b #define __SYSCALL_CONCAT(a,b) __SYSCALL_CONCAT_X (a, b) //展开得到: __INLINE_SYSCALL4 //从而4.5的结果为 __INLINE_SYSCALL4(openat, AT_FDCWD, file, oflag, mode)
4.6、然后展开INTERNAL_SYSCALL4
#define __INLINE_SYSCALL4(name, a1, a2, a3, a4) \ INLINE_SYSCALL (name, 4, a1, a2, a3, a4) //展开得到: INLINE_SYSCALL(openat, 4, AT_FDCWD, file, oflag, mode)
4.7、展开INLINE_SYSCALL
//glibc/sysdeps/unix/sysv/linux/sysdep.h
#define INLINE_SYSCALL(name, nr, args...) \
({ \
long int sc_ret = INTERNAL_SYSCALL (name, nr, args); \
__glibc_unlikely (INTERNAL_SYSCALL_ERROR_P (sc_ret)) \
? SYSCALL_ERROR_LABEL (INTERNAL_SYSCALL_ERRNO (sc_ret)) \
: sc_ret; \
})
//展开得到
INTERNAL_SYSCALL (openat, 4, args【AT_FDCWD, file, oflag, mode】);
4.8、展开INTERNAL_SYSCALL
#define INTERNAL_SYSCALL(name, nr, args...) \ internal_syscall##nr (SYS_ify (name), args) //展开得到 internal_syscall4(SYS_ify(openat), args【AT_FDCWD, file, oflag, mode】) //展开 SYS_ify(openat) #define SYS_ify(syscall_name) __NR_##syscall_name //得到 __NR_openat //从而得到 internal_syscall4(__NR_openat, args【AT_FDCWD, file, oflag, mode】)
4.9、最后internal_syscall4中,汇编调用了syscall
glibc\sysdeps\unix\sysv\linux\x86_64\64\arch-syscall.h #define __NR_openat 257
最终,syscall时,先传入调用号257,然后是四个真正的参数。