为了了解前沿技术,最近看了篇FAST23的文章,讲事务内存的。本来以为重点在内存上,结果background还没读完,直接给事务并发整不会了。然后直接上某乎恶补了一下,大概有个基本的认识了,在此记录一下。

一 事务并发的原因

事务并发主要存在的问题,简单来讲,围绕的还是事务ACID四个特性打转转。

首先是事务的基础,原子性Atomicity一致性Consistency。这两点与并发事务没有直接关系,现在主要通过 undo log和redo log两种日志保障。其中redo log用于保障事务的向后一致性,即经典的WAL(Write-Ahead Logging),通过提前把写操作记录到专门的区域,来保障即使写过程中出现问题导致写未完成,也能通过重新执行redo log中的操作,将数据更新到期望的状态——换句话说,就是大家经常提到的操作的幂等性,即允许一个操作被重复执行任意次,仍然保持正确的数据语义;而undo log用于保障事务的向前一致性,即通过记录事务修改数据后恢复到事务执行前的必要操作,保证即便在事务执行失败的情况下,可以回退到上一个合法的数据版本。但是话又说回来,历史的车轮滚滚向前,即便是撤销事务也要通过新的数据来记录这些流程,也就是说,undo log的正确性最终也还是离不开redo log的保障。

其次就是并发事务的核心所在了:隔离性Isolation持久性Persistency两位神仙打架。持久性很好理解:一个事务执行完成之后,其数据被持久化,之后对任何相关数据的读操作返回的都应该是事务执行后的结果。然而在并发这个令人头大的前提条件下,一切都麻烦了起来:受到现有计算机体系结构的限制,处理器并不能以单条指令的形式来处理每一个事务,只能通过上锁的形式来保证在当前事务执行的过程中,不被其他的事务破坏其原子性。最为传统的上锁方式就是我全都要——锁表,甚至锁库。然而这种方法性能很差,不能充分利用现有多核处理器架构的性能,因而需要通过并发的方式提升资源的利用效率。

二 事务并发带来的问题

那么该怎么办呢?既然要并发,那就不能轻易上锁,只好试着分析看看,在无锁的情况下,事务的并发会出现什么问题吧。既然数据的完整性最终通过读操作反映,那么就考虑从读操作上可能会出现的问题入手分析。一般认为,导致事务之间存在冲突的根源还是操作系统对处理器资源的时分复用,导致多个事务被同时执行,而当多个事务恰好轮流操作到同一份数据的时候,问题产生。

好在前人早已为我们做好了总结:无锁事务并发主要会遇到三种问题。依照对并发性的影响程度从低到高依次为:脏读、不可重复读、幻读。

当一个事务在执行时,读取到另一事务未提交的数据,定义为脏读。这里的“脏”数据可以理解为破坏了一致性的数据:若事务A读取到事务B修改过的数据,而事务B却在未来执行了回滚操作,而基于事务的原子性,回滚时并不考虑其它事务会读取到这份数据;那么这会导致事务A采用错误的数据继续向下执行,轻者使得该项数据错误,重者再被其他事务采用,扩散至整个数据库。

当一个事务在执行时,多次读取同一数据期间,由于另一事务修改了该项数据,导致前后读取结果不一致的情况,定义为不可重复读。考虑事务A执行过程中,事务B“插队”并且快速执行完,还修改了事务A要读取的数据。由于出现前后读结果不一致的时刻只有事务A正在执行,所以相比于脏读,读到的新数据是“干净”的,但是由于事务的持久性,又无法读出之前的旧值,因而命名为“不可重复”读。

当一个事务在执行时,多次范围查询同一数据期间,由于另一事务进行了条目的插入或者删除,导致范围查询结果不一致的情况,定义为幻读。注意到与之前不同的地方在于,这里强调了执行的操作是范围查询,且产生的不一致包含条目数量的变化,在数据形式上表现为像是食了云南VR启动器一样的幻觉,发现查询条目或多或少,因而定义为“幻”读。

三 事务并发的解决方案

兵来将挡,水来土掩。对付三种破坏数据一致性的问题,自然有四种不同级别的事务隔离,它们分别是:读未提交RU, Read Uncommitted读已提交RC, Read Committed可重复读RR, Repeatable Read串行化Serializable。实际上可以视为,每一种隔离级别都在上一种的基础上加上一条新的限制,以进一步解决新问题,同时付出一定的性能作为代价。

读未提交对应的是不考虑三种并行事务冲突场景下的隔离级别,只保证对数据访问的原子性,即同一时刻只有一个事务可以访问同一个数据;

读已提交隔离级别解决脏读的问题,有两种可能的实现方式:一是使用行级记录锁,但仅上锁保护当前正在处理的数据行;二是语句级的ReadView,可以视作数据快照,这个在后文MVCC实现思路部分将展开讨论;

可重复读隔离级别解决不可重复读的问题,同样是两种可能的实现方式:一是使用行级记录锁,上锁保护当前事务所要操作的所有数据行;二是事务级的ReadView,从逻辑上将数据库中的每行数据“锁定”为当前事务开始时的版本。

串行化再在上面的基础上解决幻读的问题,通过行级间隙锁(可以理解为范围锁)或者表级锁实现。说白了,加了这么多限制条件,约等于回到没有事务并行的时代了。

四 无锁事务并发

工程界的问题解决思路自然是捡现成的,能用就行,因而不少数据库早期均采用缩小锁粒度的方式来实现事务并发的原型版本。然而,锁终究是抢占的、单线程的,没人能保证锁在某些特定情况下不会成为性能瓶颈,那么这个锁自然是能拿掉最好,于是多版本并发控制MVCC, Multi-Version Concurrency Control诞生了。简要概括的话,MVCC采用了以空间换时间的思路,通过保留数据的历史版本的方式,来为不同事务提供各自独特版本的数据空间。

那么问题是,这个数据如何存储?会成为一项新的存储开销大头吗?

答案是否定的。还记得前面的undo log吗?在文章开始不久的地方有提到,基于undo log可以将数据恢复到历史版本,而MVCC巧妙地利用了这一点,不再额外添加新的历史版本记录,而是在实际需要旧版本数据的时候,利用undo log的差量备份,回溯到所需要的数据版本。而接下来要讨论的,就是基于undo log的MVCC基本实现方法。

不同事务操作同一数据行的时候,必然会在事务间留下中间版本。undo log记录这些中间版本的必要信息(最直接的记录方法则是直接复制,复杂点就可能是语句级别,比如delete对应insert,或update到旧数据等方式),并附加一个版本号和一个指针域,分别记录修改的提交时间和修改前的undo log条目项,称作版本链。

那么如何利用版本链实现事务并发呢?关键在于事务号之上。在MVCC中,每个事务都被赋予了一个事务号,该事务号可视作一个自增计数器,按照事务开始执行的先后顺序获取;此外,在系统中维护一组事务号信息,包含如下三大类,以MySQL为例:

  • min_limit_id:系统当前编号最小的未提交事务id;
  • m_ids:系统当前未提交的事务id列表——也就是说,min_limit_idm_ids中的最小值;
  • max_limit_id:系统将为下一个新创建的事务分配的事务id,显然永远大于m_ids中的所有事务id。

再加上事务本身被分配的事务idcreator_trx_id和读取的具体数据条目的版本号trx_id,则构成一个完整的read viewread view可以视作一个系统中当前事务进程的快照,当其条件被触发时形成,通过这个read view可以唯一确定一个可供事务使用的、正确的数据版本,具体逻辑如下:

首先前提是,对数据的读取永远从最新条目向旧版本开始搜索;

  • 如果待读取数据的trx_id小于min_limit_id,意味着当前读取到的数据版本在本事务开始执行前已经提交,对当前事务可见,返回该数据;
  • 如果待读取数据的trx_id大于max_limit_id,意味着当前读取到的数据版本非常新,甚至是在read view形成之后才提交,这意味着该数据版本一定对当前事务不可见,需要向前搜索合法的旧版本;
  • 剩下的情况集中于min_limit_id<trx_id<max_limit_id条件下,此时则依照trx_idm_ids的关系讨论具体逻辑:
    • trx_id不存在于m_ids中的时候,意味着当前事务已经提交(因为不存在分配了版本号但没开始执行的事务),那么该版本数据对当前事务可见,返回该数据;
    • trx_id存在于m_ids中的时候,属于读取到尚未提交事务的数据,具体又可分为两种情况:
    • trx_id == creator_trx_id的时候,意味着读到了当前事务自己对数据的修改,显然事务内数据是可见的,返回该数据;
    • trx_id != creator_trx_id的时候,意味着读到了别的未提交事务的数据,依照ACID原则和上面提到的并发问题,这种情况属于脏读,破坏了数据的一致性,需要向前搜索合法的旧版本。

对于以上的逻辑,简单概括还是前面提到的一句话:读取到的数据永远应该是当前状态下已提交事务的最新结果,否则向前搜索到满足该条件为止。

通过上述逻辑描述,我们可以看到的是,MVCC从设计上就避免了脏读的产生,也就是说使用了MVCC方案的并发数据库引擎天生就不会遇到脏读问题,最低是RC隔离级别。

那么可重复读和幻读呢?

答案是依然要借助之前提到的数据库隔离级别才能解决这些问题。上面提到,read view决定了当前执行的语句会读取到什么版本的数据,而满足触发条件的时候,read view形成,而这个条件正是决定隔离级别的关键:

  • 当条件为【在执行当前语句时生成read view】时,实现的是RC隔离级别,因为无法保证语句执行之间没有新的事务迅速执行并提交;
  • 当条件为【在执行当前事务时生成read view】时,实现的是RR隔离级别,因为即便在事务执行中有新数据提交,仍然会按照事务开始时生成的read view寻找更旧版本的数据。

而关于幻读的话,理论上也是予以解决了的,因为我们可以将搜索版本链而没有获得合适版本的情况视作该数据条目不存在,从而实现“屏蔽”新数据条目的问题。然而,https://www.cnblogs.com/yizhiamumu/p/16804566.html 这篇文章的作者提出了不同的意见,指出MVCC无法解决以下问题,进而认为MVCC无法处理幻读:

A:

START TRANSACTION;
SELECT FROM USER WHERE ID=5;

B:

START TRANSACTION;
INSERT INTO USER (ID, NAME) VALUES (5, 'MYNAME');
COMMIT;

A:

INSERT INTO USER (ID, NAME) VALUES (5, 'MYNAME');
COMMIT;

大意是说如果在一个事务执行的时候,新起一个事务插入一条记录并提交,再回到原有事务插入相同数据会遇到主键冲突的问题。

我的观点是,他提出的问题是存在且有意义的,但是这是写后写WAW, Write-After-Write问题,已经不属于幻读的范畴了。从上面我们的定义可以看出,本文讨论的问题主要是写后读RAW, Read-After-Write问题,无论脏读、不可重复读还是幻读都是从的角度定义的,那么就定义来看问题是解决的。

答案是锁是万能的,但是慢。

参考:https://zhuanlan.zhihu.com/p/421769708

日々编译,日々编译,烦たま死,この天天让我编译の世界早晚爆破する,この

今天的节目内容是编译Android-x86,文档给的还是比较全的,主要解决某些不存在的代码的问题。

apt update
apt -y install git gcc curl make repo libxml2-utils flex m4
apt -y install openjdk-8-jdk lib32stdc++6 libelf-dev mtools
apt -y install libssl-dev python-enum34 python-mako syslinux-utils 
apt -y install pkg-config gettext bzip2 unzip bc kmod dosfstools genisoimage

export REPO_URL='https://mirrors.tuna.tsinghua.edu.cn/git/git-repo'

cd
mkdir android-x86
cd android-x86
repo init -u git://git.osdn.net/gitroot/android-x86/manifest -b q-x86
#出现各种报错就换 mirrors.tuna.tsinghua.edu.cn/git/AOSP
find . -type f -name '*.xml' -print0 | xargs -0 sed -i -e 's#android.googlesource.com#mirrors.ustc.edu.cn/aosp#g'
sed -i -e 's#git://git.osdn.net#https://scm.osdn.net#g' .repo/manifests.git/config
find . -type f -name '*.xml' -print0 | xargs -0 sed -i -e 's#clone-depth=".*?"##g'
repo sync --no-tags --no-clone-bundle

# 你们aosp真的不考虑跟着升级一下编译器吗
cd prebuilts/gcc/linux-x86/host/x86_64-linux-glibc2.11-4.6
git fetch aosp 2078a6bf9e5479104cfe2cbf54e9602672bd89f7
git checkout 2078a6bf9e5479104cfe2cbf54e9602672bd89f7
cd ../../../../..

source build/envsetup.sh
lunch android_x86_64-userdebug
m -j16 iso_img

编译中会遇到两个问题,一个是依赖项目的-Werror,按照编译器提示加一个[[fallthrough]];然后继续编译就行;第二个参照https://stackoverflow.com/questions/67557000/depmod-is-not-allowed-to-be-used ,修改build/soong/ui/build/paths/config.go,添加"depmod": Allowed,即可。解决这两个问题后应该就能正常完成镜像的生成了。

libvirt是红帽家写的一套虚拟化软件管理工具。

现在市面上有着大量的虚拟化软件,大至ESXi、Proxmox,小如LXC、QEMU,但这些工具提供了各自不同的接口,出现混用的情况将会十分麻烦。俗话说的好,没有什么是计算机工程无法解决的,如果有,那就加一个抽象层。而libvirt则提供了一个统一的接口,允许以相同的格式在不同底层平台上创建对应的虚拟实例,而这也可以说是红帽家OpenStack允许在混合云环境上搭建虚拟化平台的技术支柱之一。

既然libvirt叫做而不是工具,那么意味着其使用方法更多地是在代码中调用,而非作为一套命令行工具由人工手动操作。然而咱作为普通用户,没有数据中心那个经济实力,一套OpenStack开下来很有可能一半内存就没了(单机内存占用8G起步),因而只能试着用用底层工具来自行创建和使用虚拟机了。

接下来将使用同样是libvirt包提供的virsh及其衍生工具virt-install来创建虚拟机。

首先是安装软件包:

sudo dnf install -y qemu-kvm libvirt virt-install

如果机器配置了图形界面的话,可以安装virt-viewer包,执行virt-install时可以直接给出窗口展示虚拟机的图形界面。由于我这里服务器没配图形界面,后面就都是用的vnc了。

由于virt-installqemu的命令风格非常相似,因而可以参考我之前介绍qemu的文章,这里直接贴命令行了:

virt-install \
--virt-type=kvm \
--name win10 \
--ram 8192 \
--vcpus=4 \
--os-variant=win10 \
--boot cdrom \
--disk <Windows_ISOFILE>,device=cdrom,bus=sata,readonly=on \
--disk <VirtIO_ISOFILE>,device=cdrom,bus=sata,readonly=on \
--graphics vnc \
--disk path=<PATH_TO_STORE_IMAGE>,size=100,bus=virtio,format=qcow2

接下来是通过vnc远程连接虚拟机图形界面的流程。由于偷懒,这里选择不用安装客户端的novnc:

dnf install -y novnc

默认的vnc端口是5900,我们需要配置novnc连接到该端口,然后再从新端口提供novnc解析后的vnc客户端服务:

novnc_proxy --vnc localhost:5900 --listen 8443

这里选择了8443端口提供novnc服务,在浏览器中输入ip-端口组合即可通过浏览器访问虚拟机的vnc界面了。不过我这挺怪的,novnc的某些资源加载的时候会随机报404,只有反复刷新,等所有资源都被缓存好之后才能正常使用= =

给巨硬一点小小的FA♂国震撼

起因是发现UUP dump上竟然可以打包aarch64的镜像包,于是准备捞一份10和一份11下来做纪念。结果捞下来以后就想着,下都下好了,不跑起来用用真是怠慢了它们。转念一想,VMware、VirtualBox、Hyper-V这种虚拟机都是同架构下做虚拟化的,性能好是好,但是不支持跨指令集,只好借助开源而万能的QEMU了。

在QEMU上运行Windows ARM版本与实体机器上的流程基本一致:加载固件、启动安装盘、补驱动和安装使用。相比于实体机按个开机键就能够自动加载BIOS,在QEMU中需要手动指定二进制文件作为BIOS。由于ARM处于百花齐放的状态,不能保证像x86一样有统一的Legacy启动模式,因而往往采取新兴而统一的UEFI来引导系统。

一 系统固件准备

系统固件可以认为是一段可以执行的机器码,与硬件环境有强相关性,负责初始化各类硬件设备,以及告诉操作系统如何使用这些设备。我们一般能接触到的UEFI固件通常是TianoCore的EDK,这是一个由Intel主导的社区实现的开源版本,因而不少人选择基于这个EFI固件魔改代码来支持不同平台,今天的主角QEMU正是其中的目标平台之一。Linaro作为ARM产业链中的中性组织,提供了ARM平台下编译好的参考二进制固件(https://releases.linaro.org/components/kernel/uefi-linaro/)。

但是在经过尝试发现,用linaro提供的固件引导Windows无法正常启动,因而在后续流程中,本文只得改用别人魔改的固件。虽说不要轻易采用来路不明的二进制程序,但在眼下并不良好的环境(指代码和文档稀缺),加之学习为主的目的,只得暂且捏着鼻子先用着了。

别人提供的二进制固件可以在GitHub上下载:https://github.com/raspiduino/waq/releases ,里面的vm.7z中解压出来的QEMU_EFI.imgQEMU_VARS.img两个文件就是所需要的UEFI固件。

二 虚拟机配置与启动

QEMU是个定制化程度较高的虚拟机软件,因而默认配置提供的是一个光秃秃的机器,可以认为是只有处理器和内存,完全满足不了运行Windows的需求——显示器、鼠标键盘等交互设备全部需要自己添加。

在这一步需要准备的东西有三个:

QEMU的虚拟机设备需要通过命令行配置,这里给一个我的模板:

qemu-system-aarch64 ^
-M virt,virtualization=true ^
-accel tcg,thread=multi ^
-cpu cortex-a57 ^
-device VGA ^
-smp 4 ^
-m 4G ^
-drive if=pflash,file=QEMU_EFI.img,format=raw ^
-drive if=pflash,file=QEMU_VARS.img,format=raw ^
-device qemu-xhci ^
-device usb-kbd ^
-device usb-mouse ^
-device intel-hda ^
-device hda-duplex ^
-nic user,model=e1000 ^
-boot d ^
-device usb-storage,drive=install -drive if=none,id=install,format=raw,media=cdrom,readonly=on,file=<Windows镜像> ^
-device usb-storage,drive=drivers -drive if=none,id=drivers,format=raw,media=cdrom,readonly=on,file=<virtio驱动镜像> ^
%*

该模板从上到下依次配置了机器类型、硬件加速模式、处理器型号及数量、内存、显示设备、系统固件、外围硬件,以及系统盘和驱动盘。需要注意的是,机器类型最好开虚拟化参数(即virtualization=true),否则可能启动黑屏;系统安装盘和驱动盘要走USB设备而不能是virtio设备,否则会缺驱动,导致读不到数据,无法安装系统;最后的%*表示允许从命令行继续添加参数,比如加个系统盘,或者其他QEMU设置。在接下来的文字中,我会将这段命令行保存为run_win10.bat并直接使用。

如果仅仅是尝试是否能够正常启动的话,此时直接双击运行run_win10.bat就可以启动虚拟机做测试了。

三 安装系统

如果打算尝试体验,则需要创建并挂载一个虚拟硬盘,用于安装系统。

使用qemu-img工具创建虚拟磁盘:

qemu-img create -f raw system_10.img 20G

此时会在当前目录下创建一个大小为20G的img文件,但在ext或者NTFS这类支持文件空洞的文件系统上,实际占用空间为0,文件的实际大小会随着向内写入数据逐步扩大,直至充满声明的文件大小。

如果文件大小为0的话,表示虚拟硬盘创建并未成功,在虚拟机里显示的磁盘大小为0,此时可以通过qemu-img resize命令来补救:

qemu-img resize -f raw system_10.img 20G

此时应该正常显示文件大小为20G,而实际占用空间为0。经测试,安装系统并完成OOBE后空间占用大约在10G左右。

此时可以通过如下命令,启动一个可以安装系统的虚拟机:

run_win10.bat -drive if=virtio,format=raw,file=system_10.img

如上面所提到,这条命令在之前的基础配置上额外添加了刚创建的system_10.img作为系统盘,并启动虚拟机。该系统盘的硬件类型为virtio-blk,性能相对较好,但需要手动安装驱动。

启动虚拟机后,会发现在选择安装盘步骤找不到任何硬盘,此时则需要在左下角加载驱动程序,运气好的话系统会自动搜索光驱,并提供可供安装的驱动;否则则需要手动浏览文件夹,选取viostor\w10\ARM64目录,安装程序才能找到合适的驱动程序;Windows 11同理。

接下来就是常规的安装流程了,与x86电脑上的流程并无大异。不过在OOBE阶段,可能会出现OOBEKEYBOARDOOBELOCAL等错误信息,能跳过的直接跳过就行,如果不能跳过且一直重试失败的话,则可以用组合键Shift+F10调出cmd窗口,输入shutdown -r -t 0软重启后重新开始配置。

四 使用系统

刚进入系统的时候,巨硬会后台起一个索引程序,我给了四核的处理器直接拉满,网上说有办法停下来,有需要的可以自行搜索,在此不作赘述。

不过要吐槽的是右键菜单,在桌面右键想看看系统信息都能给我卡半天= =一开始以为是索引吃CPU导致卡顿,结果后来一看他COM用的竟然还是x86的,相当于x86经过QEMU转成arm64之后,巨硬又给转回x86指令了。。大哥啊这都系统基本组件了,重新写份原生的有那么难么。。。

另一个问题是显示分辨率问题,默认是800*600,看的那叫一个难受。系统里是不能调节的,我在网上找了半天,说是QEMU配个标准VGA设备能支持到1080p,然而倒腾了半天都没用= = 最后才发现系统的UEFI固件里有设置选项,可以调成1080p。

最后一个问题是网络,这个暂时就没研究了,大伙也说网络问题不少,之后有精力再来折腾吧。

有些人就是喜欢折腾

最近又心血来潮,打算把两年前的坑填上,整了两天,算是把部署OpenStack的坑踩得差不多了。部署方法和之前一样,还是采用的kolla-ansible,主要是neutron_external_interface的文档还是写的比较模糊,加上OpenVirtualSwitch一起,折腾了不少时间。

总结起来,这次折腾的主要结论是,能用,但没必要。由于OpenStack本来就是工业级的设计,开发者原本就是基于企业环境控制网络和外网分离的假设做的实现,因而把两个网络合并势必会受到操作系统等外部组件的设计阻碍,这次的坑基本上都是来自这方面。

首先一开始是打算虚拟一个接口给外网,然后用系统做流量转发给物理网口,但是因为技艺不精,一直失败,只好作罢。后来看到OpenStack的开发文档中提到,由于开发者电脑不一定像工业级服务器一样有两个网口,因而还是允许使用单网口机器部署的。只不过由于ovs要独占外网网口,对外网的访问要从原本的物理网口转移到虚拟网口,导致网络配置丢失,因而必须要使用非远程的方法进行配置(BMC除外)。

大致配置流程如下(假设网卡为eth0):

  1. 按照正常流程配置OpenStack安装,并将network_interfaceneutron_external_interface都设置为eth0
    安装到一半的时候,由于kolla-ansible配置了ovs,导致原有网络配置失效,无法访问外网,表现为某些docker pull失败,说无法连接到quay.io。
  2. 此时可以发现eth0被连接到了ovs-system,但是用ovs-vsctl查看,发现eth0又被分配给了br-ex。用nmcli手动进行网络地址的配置:
    nmcli connection modify br-ex connection.autoconnect yes
    nmcli connection modify br-ex ipv4.addresses xxx.xxx.xxx.xxx
    nmcli connection modify br-ex ipv4.gateway xxx.xxx.xxx.xxx
    nmcli connection modify br-ex ipv4.method manual
    nmcli connection modify br-ex ipv4.dns xxx.xxx.xxx.xxx

    其中xxx.xxx.xxx.xxx设置为网卡正常访问外网时的配置即可。需要注意这几条命令的顺序最好不要修改,因为nmcli会做参数检查,如果没设置好某些参数是设置不了另一些的。

  3. 执行nmcli device set br-ex managed yes,将br-ex的网络配置交给nmcli执行,此时可以用ping检查网络是否配置完成;
  4. 修改kolla-ansibleglobals.yml,将network_interface配置为br-ex,因为此时eth0已经无法正常用于访问网络;
  5. 重新执行kolla-ansibledeploy指令,等待部署完成即可。

由于nmtui似乎没法快捷方便地管理ovs的网卡配置,因而每次重启后需要手动将br-ex设置为managed模式,才能由NetworkManager进行配置,即手动执行第三步。

接下来如果有新坑的话,就是在Linux上配个子网,然后让OpenStack用本地虚拟子网作为外网了