Skip to main content

ucb cs186 课程笔记(更新中)

· 8 min read
ayanami

lec2

join: inner join, natural join, outer join

sql 实际执行模型 写起来是 SELECT - FROM - GROUP BY - HAVING - WHERE - DISTINCT - ORDER BY

实际是 FROM(table过滤) - GRUOP BY(行分组) - HAVING(组过滤) - WHERE(行过滤) - DISTINCT(行去重) - SELECT(行内列过滤)

inner join:叉积,对AB所有行组合

SELECT * FROM TABLE1 t1, TABLE2 t2 
WHERE t1.id = t2.id
AND ...
-- 等效于
SELECT * FROM
TABLE1 t1 INNER JOIN TABLE2 t2
ON t1.id = t2.id
WHERE ...
-- 下面这种更加清晰一点
-- 等效于
SELECT * FROM
TABLE1 t1 NATURAL JOIN TABLE2 t2
WHERE ...
-- natural join就是在组合的基础上自动用了一个过滤,要求table所有相同名字的列的值都相同

outer join:

Left Outer join:

A LEFT OUTER JOIN B ON cond 如果cond满足的话,得到的是AB的组合(一行有A的列+B的列);如果不满足,得到A的列+空

Right Outer Join 同理

Full Outer Join 同理 例如ON A.id = B.id

如果有A没有对应的B, 那就是是 A + 空

如果有B没有对应的A, 那就是 空 + B

非常好的图

db-join

alias

简化 + 看起来更清楚(尤其是self-join)

FROM TABLE1 AS x, TABLE1 AS y

String Comp

LIKE或者正则S.name ~ '^B.*' (等效于S.name LIKE 'B_%')

AND OR 做条件交并

EXCEPT UNION (ALL) INTERSECT做子查询结果集合的交并差

IN EXISTS用于子查询 (NOT IN, NOT EXIST) EXISTS是判空

SELECT S.sname FROM Sailors S WHERE S.sid IN 
(SELECT R.sid FROM Reserves R WHERE R.bid=102)

还有ANY ALL

ARGMAX?

SELECT * FROM Sailors S WHERE
S.rating >= ALL
(SELECT S2.rating FROM Sailors S2)

View: Named Queries

CREATE VIEW xxx
AS ...

SELECT * FROM xxx;

cache and reuse

或者

WITH [viewname] AS [statement]创建一个临时view

NULL 参与的运算大多是NULL, 除了IS NULL,False AND NULL这种

lec3

Disk & Buffer

整体架构

SQL client-> Query Parsing & Optimization->Relational Operators-> Files and Index Management->Buffer Management->Disk Space Management

Concurrency Control & Recovery

磁盘太慢,需要尽量减少读写,且寻道和旋转时间是大头

"block" && "page": 一个意思,磁盘上的块状读写最小单元 一般64KB-128KB

为了重用硬件驱动,经常会将磁盘空间管理器建立在文件系统API上,但带来了一些大数据库多文件系统的问题,也有直接建立在设备上的,更快但是移植性问题

给上层的抽象是一个巨大的文件

DB file: page的集合,每个page又包含了许多records

给上层提供:CRUD on records

record解构成一个"指针" {pageID, location on page}

structures

  • Unordered Heap Files(和数据结构heap没啥关系,无序records)
  • Clustered Heap Files
  • Sorted Files
  • Index Files

如何组织page呢?

链表? 想想就知道效率很差

类似目录的形式? 部分page只存到其他page的指针,并且始终放在缓存之中

page解构

Page Header:

  • Number of records
  • Free space
  • Mayba a last/next pointer
  • Bitmaps, slot table

record 中间留不留空?

不留空:Fixed Length Records, Packed

header后面跟紧密定长records, 因此可以有 record id = {pageId, record number in page}, 简单运算得到location

加很简单,直接append

删,全移一遍?->O(N),自然想到能不能lazy delete或者soft delete

方法是在header里面放一个delete bit的bitmap

变长?

slotted page

将信息存在footer(称为slot directory), record从头部开始存

由record id得到dir中位置,位置里面是pointer + length,

删,将slot dir中的项置空

插入,插在空位上,更新slot dir

fragmentation?

什么时候reorganize?->设计取舍,大部分时候没有那么多删除(乐)

slot不够->从page尾部向前增长

lec4

cost model for ayalysis

B, D, R

  • the number of data blocks
  • the number of records per clock
  • avg time to r/w disk block
  • opt: index

indexes:

大幅度降低range操作耗时

An index is data structure that enables fast lookup and modification of data entries by search key

区间查找 & 子集搜索, 可以复合, 不需要唯一

2-d box 2-d circle n-d indexes都有

kd树啊R树啊

postgres 的 GiST index

left key opt: 最小的key是不需要的,直接拿-inf当下界就行

处理相等:>= 向右走就行

B+树

  • 叶子不一定是连续的-动态分配,指针连接以支持range scan

  • 阶数d, fan-out 2d+1 典型的fan-out 为2144()

  • 删除, 理论上来说, 可能涉及到重新平衡等操作 但实际的操作之中, 只需要删除即可, 原因是平衡太慢了,并且删了也能再插

叶子放什么?

  1. 数据

pros:

cons:

  • 想要在另一列构建索引只能重新复制文件(文件只能按照一种方式实际排序存储)
  • 即使真复制了,同步问题也很寄
  1. 指向数据的指针 (key, page id+list of record id)

在b+树里面有重复项

  1. 指向同一个键的所有records (key, list of (page id + list of record id))

减少冗余,增加复杂性

clustered: index指向的数据块在磁盘上是按照这个index排序或者近似排序的

非常大影响性能 顺序比随机快100倍

对于一个有变化的数据,例如插入或者删除,需要一些成本进行磁盘数据的重新排序来维持clustered

B+树的平衡性:

使用字节数半满(占页面容量)就行, 甚至实际上更低, 按照实际性能来决定,不严格

变长key: 前缀压缩 trie

性能的常数:

由于顺序读写比随机读写快100倍

B+树比全表扫描差不多也是涉及到1%以下的表才有显著优势

所以例如对一个非聚簇索引进行一个跨越半个表的range的扫描, 那还不如直接把全表取出来

优化

由于B+树效率真的很低,所以有很多优化策略

  • bulk loading 批量装载
  1. Sort the data by a key.
  2. Fill leaf pages up to size f (the fill factor).
  3. If the leaf page overflows, then use the insertion split algorithm from a normal B+ tree.
  4. Adjust pointers to reflect new nodes if needed.

NJU操作系统(jyy OS)课程笔记-虚拟化部分

· 18 min read
ayanami

lec14 操作系统上的进程

cpu有初始pc地址->放置固件上的初始程序(固件状态机)->启动OS(os状态机)->load init程序(程序状态机), 之后OS完全把行为转交给init(进程树的root)

llm 知道存在知道的界限正在模糊: 知道存在且合理 逐渐趋同于 能做

例如 qemu 相关的一些东西

问llm发散出的概念->知识体系的快速建立

fork? 以状态机的视角理解

经典的for fork + printf

写了个示例

#include <cstddef>
#include <cstdio>
#include <cstdlib>
#include <stdio.h>
#include <unistd.h>
#include <vector>
#include <mutex>
#include <sys/wait.h>
#include <map>
#include <string>
using namespace std;
const size_t buf_size = 1024;
const std::map<int, std::string> mode_map = {
{_IONBF, "no buffer"},
{_IOLBF, "line buffer"},
{_IOFBF, "full buffer"},
};
void test(int __modes) {
printf("test in mode %s\n", mode_map.at(__modes).c_str());
fflush(stdout);
vector<int> childs;
std::mutex mtx;
setvbuf(stdout, nullptr, __modes, 0);
for (int i = 0; i < 2; ++i) {
int pid = fork();
printf("hello from pid %d\n", pid);
if (pid > 0) {
std::lock_guard<mutex> lock(mtx);
childs.push_back(pid);
}
}
}

int main() {
// _IOLBF, _IOFBF, _IONBF
test(_IOFBF);
printf("\n");
fflush(stdout);
return 0;
}

_IOLBF_IONBF的情况下会出来6个hello

每次printf都直接刷新/检测到换行符刷新缓冲, fork的时候没有IO状态 而_IOFBF会有8个hello, 在fork第二次的时候会带着缓冲区(就是一段内存空间)进行fork,所以最后的4个进程每个都带着2个hello

系统里面没有魔法

fork: 把所有的知道的不知道的都复制了

“是不是这样?” -> 不知道的底层状态被复制了

execve: 重置状态机 argc, argv, envp -> main()

execve是唯一一个可以新建一个状态机的系统调用

exit?

  • main return
  • exit libc提供的
  • _exit 系统调用退出(== asm volatile("mov ..., %rax; syscall")
  • 直接SYSCALL

前两个在c语言的空间, 是“normal exit”

后两个不是normal的, _exit exit_group , __exit exit self

行为区别? strace

lec15 进程的地址空间

pmap

/proc/[pid]/maps

vvar(r), vdso(rx), vsyscall

os内只读的syscall -> 可以以内存的形式共享

其实只需要进程能和OS交互一些数据就行 —— why not进程写page, OS轮询?

  • 在极端的时候能提高一些高优先级的进程的性能, 某篇OSDI

地址空间应该是在运行时可变的

所以我们需要一个不存在于c世界的操作(syscall)去操作地址空间 -> mmap, munmap

入侵进程的地址空间: gdb, perf

Game Genie 物理入侵地址空间

  • 外接电路: 当cpu读地址a的时候读到x, 则替换为y

jyy现场演示mini CE(雾)

gdb attach到虚拟机,查找满足某个模式的内存值, 修改之

/proc/[pid]/mem 修改器 = 调试器

xdotool: cmd X11 automation tool

ydotool: better xdotool -> 按键精灵

evdev 按键显示脚本

xdotool测试vsc插件, crazy

或许不需要那么多的“魔法工具”

OS: 解放编程能力, 什么事情在OS上可以做

变速齿轮: syscall是感知时间的唯一方法

gdb 脚本之中, 在gettimeofday打断点, 然后修改寄存器, amazing!!!

hook

patching: 整活, kpatch, 不停机更新(软件动态链接)

old func, rx -> 修改为rwx -> 修改old func为, jmp到new func

在chcore里面看看? 或许有必要研究一下gdb(attach with qemu)

lec16 syscall & unix shell

everything is a file

thing: 操作系统里面的对象

gpt时代的“编程”——自然语言?

//OS: API:
// get_object_by_name(
// "the address space file of pid=1234"
// )

文件描述符: 指向OS对象的“指针”

windows: handle(句柄)

IPC endpoints: 例子, 管道

管道是同步的

fork + pipe? 本质是"指针"的拷贝

现在两个进程都有读口和写口啦

shell, kernel 的外壳

cli: 高效简洁的编程语言

算力的提升: cli -> gui -> 自然语言

shell as pl: 基于文本替换的快速工作流搭建

job control: 类比窗口管理器的"x", 最小化

或许不需要tmux, shell就是最简单的tmux

手册: complete ref

AI是“被动的”, 读一读shell manual

复刻unix shell

“抛开系统库”

-ffreestanding -nostdlib -static

gdb init已经很常见了, 但gdb init到python再在python里面转回/proc/[pid]/fd打印, 最后结合gdb的内置hook,在stop时候打印, fancy!

这打印的不是我们go的channel语法吗, 更有趣了

sh manual

lec 17 syscall的封装: libc

pipe write如果小于PIPE_BUF, 是原子的

pipe 7

读者关闭: Broken pipe

libc 标准化, 稳定可靠, 移植性极好

C runtime library: -Wl, --verbose看到链接列表

调试glibc? 历史包袱重, 大量内联汇编, musl

只要实现了C ABI指定的堆栈排布的系统调用, 就可以轻松移植musl等到自己的OS上, 底层的计算由硬件指令集给出

System V ABI

脱开workload 做优化就是耍流氓

  • 在开始考虑性能之前, 理解需要考虑什么样的性能

workload哪里找? 当然是paper了(顺便白得方案)

  • 看wkld调性能

mm alloctor: 根基

  • 大对象应该有长生存期, 否则是performance bug
  • 越小的对象创建/分配越频繁
  • 小对象, 中对象, 大对象

瓶颈几乎是小对象

链表/区间树不是一个好想法: 上锁, 不能很好的并行化

设置两套系统:

  • Fast path 性能极好,并行度极高,覆盖大部分情况
  • Slow path 不在乎速度,但把困难的事情做好
  • 例如cache

init ram fs

ISA -> OS 对象/syscall -> libc -> 系统工具 coreutils, busybox -> 应用程序

initramfs

  • 加载剩余必要的驱动程序, 例如磁盘/网卡

  • 挂载必要的fs

  • 将根文件系统和控制权移交给另一个程序, 例如systemd

initramfs作为一个非常小的启动fs, 再把磁盘这个OS Object mount进来, 最后switch root把控制权给到磁盘的的根系统

启动的第二级阶段 /sbin/init

疯狂的事情不断有人在做, 但疯狂的事情的起点其实经常很小

lec 19 可执行文件

elf不是一个人类友好的“状态机数据结构描述”

为了性能, 彻底违背了可读(“信息局部性”)原则

可执行文件=OS的数据结构(core.dump), 描述了程序应该的初始状态

支持的特性越多, 人类越不能理解

人类友好: 平坦的

回归连接和加载的核心概念: 代码、符号、重定位

my_execve

elf file -> parse as struct

-> 将各个section load到指定的地址(mmap)->asm volatile布置好ABI调用栈(根据手册)->jmp!

如何释放旧进程的内存资源?proc里面需要有记录

lec 21 syscall & ctx switch

dynamic linker

se给的os基础还是很扎实的 很难想象ics2里面讲了GOT和PLT

SEE ALSO是一个宝藏 man ld.so

hacking: LD_PRELOAD不需要修改libc, 动态加载的全局符号, 先到先得

劫持大法

kernel memory mapping

低配版Linux 1.X 分段, 内核在低位, 只是分个段

低配版Linux 2.X 内核还是在物理低位, 但程序看到虚拟地址已经是高位了

today: complete memory map

qemu is a state machine simulator: 调试syscall(gdb并不能si从用户态进kernel)

另一种理解中断的方式:"被"插入一条syscall

中断, 把状态机的整个寄存器状态存到内存里面

在汇编之中小心排布内存和搬运寄存器, 返回到c之中就是结构体的context

schedule的核心: 调用一个“不会返回的函数”

这个(汇编)函数以context为参数, 并且根据context, 返回到另一处控制流...

-> coroutine 也是如此! OS作为一个“状态机管理器”就在做一个"coroutine event handler"的作用

lec 22 process

进程: “戴上VR”的thread

有自己的地址转换, 对一切的load/store会应用一个f,作用在addr上

硬件提供了“戴上VR”的指令

这个f从ds的视角来说就是int->int的映射

查页表(int->int的映射)这件事, 如何加速? --自然想到radix tree

普通实现是radix tree(x86, riscv, ...收敛到的最终方案)

每一次访存都要查这么几次的话不可接受

因此有了TLB, 但立刻带来的一个设计问题是, 谁来管TLB(以及对应的miss处理?)

x86选择放到硬件, 但丧失灵活性的后果是即使有些进程只想要f(x)=x, 也必须要老实查表, TLB在和cpu cache抢带宽

MIPS选择放到软件, miss了直接丢出来异常, 让软件来决定怎么处理TLB

疯狂的想法: inverted page table

把key从VPN换成 (VPN, pid), 然后从一一映射改成hashtable, 支持每个进程有自己的页表

缺点在例如hashtable带来的冲突时(TLB miss, etc)时间不可控(O(1) ~ O(n))

每个进程都有自己的“VR眼镜”这件事情还带来了更多的优化空间, 例如多个进程, 不同的虚拟地址块映射到同一个物理地址, 以及cow

KSM(kernel samepage merging/mermory deduplication), demand paging

fork: 进程快照, redis

cow fork的缺点: 让系统实现变复杂

改革: 砍掉所有的内核部分, 剩下的全部交给xv6

lec 23 处理器调度

trampoline code

跳板代码, 例子

  • call printf -> call *GOT(printf)
  • JIT编译器
  • 软件热更新(patch 函数头)

资源调度(分配)是一个非常复杂的问题

建模, 预测, 决策 -> 调度策略的设计空间

调度策略

再加一层机制 "niceness", 管理员控制nice, 越nice越能得到cpu

10 nice ~ 10倍性能差异

taskset 绑定一个process到一个cpu上

round-robin时代: MLFQ, 动态优先级

  • 让出CPU(I/O) -> “好”

  • 用完时间片 -> “坏”!

1960s: breakthrough!

2020s: 对很多负载都欠考虑

今天的调度: CFS(complete fair scheduling)

但有vruntime, "好人"的钟快一些

真实的处理器调度: 不要高兴得太早...

  • 低优先级的在持有mutex的时候被中间优先级的赶下处理器, 可以导致高优先级的任务等待mutex退化到低优先级 -> 火星车

Linux: 没法解决, CFS凑合用

实时系统: 火星车在CPU Reset, 不能摆烂

  • 优先级继承, 条件变量唤醒?

  • lockdep预警

  • ...

然而不止有锁, 还有多处理器...

今天的计算机系统: SMP

多处理器的矛盾困境

  • 绑定一个线程:"一核有难, 八方围观"
  • 谁空丢给谁: cache, TLB白干

更多的实际情况: NUMA, 异构, 多用户

  • numa: 远近cpu性能差达到数倍

  • 多用户的cpu共享? namespaces, cgroups, 例如一个程序开并行, 另一个程序是串行的, 是否需要给串行的保留一个核, 而不是开得越多抢得越多

  • 异构, 大小核超小核, GPUNPU, 每个核的独有缓存和共享缓存...

  • 更少的处理器可能更快...(反直觉, 同步cacheline带来的开销)

复杂的系统无人掌控

ghOSt: Fast & Flexible User-Space Delegation of Linux

开始下放给应用程序做调度

Others

早期优雅的设计可能会成为后续发展的包袱: fork+exec带来的膨胀, 所有涉及到OS内部状态的api都需要考虑fork行为, 例如文件偏移量...

总线, 中断控制器, DMA

总线: 提供设备的“虚拟化”, 注册和转发, 把收到的地址(总线地址)和数据转发到对应的设备上

这样cpu只需要直连一根总线就行了!

PCI总线

  • 总线可以桥接其他总线, 例如pci -> usb

lspci -tv可视化

"即插即用"的实现——非常复杂!

cpu: 只有一根中断线

启动多个cpu: cpu给其他cpu发中断!

中断仲裁: 收集各个设备中断, 选一个发给cpu

APIC(Advanced PIC):

  • local APIC: 中断向量表, IPI, 时钟, ...
  • IO APIC: IO设备

DMA: 很早期就有了, 解放cpu, 设计专用的电路只做memcpy

今天: PCI总线直接支持

文件 = 实现了文件操作的“Anything”

设备驱动程序: 一个 struct file_operations的实现, 就是一段普通的内核, “翻译”read/write等系统调用

/dev/null的驱动: read永远什么都不做返回0, write永远什么都不做返回count

一种"duck type"

设备不仅仅是数据, 还有配置

配置设备:

  • 控制作为数据流的一部分(自定义一套write的指令编码)
  • 提供一个新的接口

ioctl: 非数据的设备功能几乎完全依赖ioctl, 完全由驱动决定

数量最庞大,质量最低的shit

unix的负担: 复杂的hidden spec

/dev/kvm 硬件虚拟化, 支撑了几乎所有的云产商虚拟化方案

unix的设计: 目录树的拼接

将一棵目录树拼到另一棵上

回想最小linux系统, 只有/dev/console和几个文件

/proc, /sys, /tmp都是mount系统调用创建的

"看到的fs!=磁盘的fs", is just a view

像是procfs这种并非实际的fs更是, 可以挂载到任意的地方, 以任意的数量(因为他只是fake了read/write的“file Object”)

根本设计哲学: 灵活

灵活性带来的

  • /, /home, /var都可以是独立的设备, 把有些快的放在一个目录存可执行文件, 另一些存数据...

mount一个文件? loopback device

设备驱动把设备的read/write翻译成文件的rw

FHS: Filesystem Hierarchy Standard

ln -s 图结构 as 状态机

fs: 一个”数据结构题“, 但读写的单元是一个block

FAT: 集中保存所有"next"指针, 可靠性? 存n份!

fat manual

fat 小文件ok, 大文件不行

来本地部署大模型!

· 4 min read
ayanami

前言

这件事情的起因是这样的, 在开卷上机考想要部署一个本机大模型参考一下, 同时有同学和我讲qwen2.5-coder-7B非常的nice, 于是就有了下面这篇文章, 用ollama + docker部署的local LLM...

本地环境: Ubuntu24.04

以下是步骤

下载nvidia docker runtime

参考 https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html#installing-with-apt

apt

curl -fsSL https://nvidia.github.io/libnvidia-container/gpgkey | sudo gpg --dearmor -o /usr/share/keyrings/nvidia-container-toolkit-keyring.gpg \
&& curl -s -L https://nvidia.github.io/libnvidia-container/stable/deb/nvidia-container-toolkit.list | \
sed 's#deb https://#deb [signed-by=/usr/share/keyrings/nvidia-container-toolkit-keyring.gpg] https://#g' | \
sudo tee /etc/apt/sources.list.d/nvidia-container-toolkit.list
sudo apt-get update
sudo apt-get install -y nvidia-container-toolkit

设置 /etc/docker/daemon.json

{
"default-runtime": "nvidia",
"registry-mirrors": [
"https://1nj0zren.mirror.aliyuncs.com",
"https://docker.mirrors.ustc.edu.cn",
"http://f1361db2.m.daocloud.io",
"https://registry.docker-cn.com"
],
"runtimes": {
"nvidia": {
"args": [],
"path": "nvidia-container-runtime"
}
},
}

如果你需要代理, 参考配置加上

   "proxies": {
"http-proxy": "http://127.0.0.1:7890",
"https-proxy": "http://127.0.0.1:7890",
"no-proxy": ""
}

然后重启docker服务

sudo systemctl daemon-reload    
sudo systemctl restart docker

出现找不到"nvidia" runtime错误的, 检查有没有下载过docker desktop

下载过docker desktop的:

docker context ls
docker context use default

切换回default, 然后重启docker服务

下载ollama镜像

mkdir -p /data/containers/ollama/data
vi /data/containers/ollama/docker-compose.yml

docker-compose.yml

name: 'ollama'
services:
ollama:
restart: always
image: ollama/ollama
container_name: ollama
runtime: nvidia
environment:
- TZ=Asia/Shanghai
- NVIDIA_VISIBLE_DEVICES=all
networks:
- ai-tier
ports:
- "11434:11434"
volumes:
- ./data:/root/.ollama
networks:
ai-tier:
name: ai-tier
driver: bridge
ipam:
config:
- subnet: 172.22.1.0/24

启动

cd /data/containers/ollama
docker compose up -d

之后会拉ollama (2G)

验证成功

docker compose ps
# 得到结果应该如下
NAME IMAGE COMMAND SERVICE CREATED STATUS PORTS
ollama ollama/ollama "/bin/ollama serve" ollama About a minute ago Up About a minute 0.0.0.0:11434->11434/tcp, :::11434->11434/tcp

下载模型

qwen2.5:7b建议换成其他的代码专用模型, 根据自己的电脑显卡配置决定参数量

空间占用 7b:5G, 3b: 2G, 1B:1G

docker exec -it ollama ollama pull qwen2.5:7b

成功结果这样

pulling manifest
pulling 00e1317cbf74... 100% ▕████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████ 4.7 GB
pulling 4fa551d4f938... 100% ▕████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████ 12 KB
pulling 8ab4849b038c... 100% ▕████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████ 254 B
pulling 577073ffcc6c... 100% ▕████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████ 110 B
pulling ad1518640c43... 100% ▕████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████ 483 B
verifying sha256 digest
writing manifest
removing any unused layers
success

验证

❯ docker exec -it ollama ollama list
NAME ID SIZE MODIFIED
qwen2.5:7b 845dbda0ea48 4.7 GB 2 hours ago

What's next:
Try Docker Debug for seamless, persistent debugging tools in any container or image → docker debug ollama
Learn more at https://docs.docker.com/go/debug-cli/

开始服务

docker compose up -d

会在localhost:11434起一个服务, 浏览器输入后正常会有Ollama is running

前端套壳

ChatBox

直接去官网下载

https://chatboxai.app/zh/install

设置里面指定一下模型

image-20241121211509468

aider版本

参考https://aider.chat/docs/config/dotenv.html设置一下OLLAMA_BASE_API的环境变量

之后aider --model ollama/qwen2.5:7b 即可

下载自己看官网(pip install aider-chat)

ok, 大功告成!

[可选] IDE插件

一个例子是Continue插件https://www.continue.dev/

参考官网, 据说vsc支持还行, jet bug不少

ostep阅读笔记:单机fs的崩溃一致性(chapter42-44)

· 10 min read
ayanami

chapter 42 崩溃一致性

以一个传统的结构(linux ext2)为例子

一个磁盘group

inode bitmap | data bitmap | inode block | data block

磁盘映像

super | group0 | group1 | …

想要给一个文件追加一个block,需要改inode block, data bitmapdata block 3处

设想中途断电,硬件原子性在磁盘上是不好做的,所以可能在三个写入之中发生任意个写入落盘的情况

断电时已经修改的数据和后果的对应:

  • inode block, 元数据不一致,指向垃圾数据
  • data bitmap,元数据不一致,空间泄露
  • data block, 没关系
  • inode block + data bitmap, 文件是乱码,问题不大
  • inode block + data block,元数据不一致
  • data bitmap + data block, 元数据不一致,空间泄露

早期文件系统:fsck,让不一致发生,重启时修复,只确保元数据一致

  • 检查super block(发现系统大小小于分配块数等不健全的情况,可以考虑启用super block的备用副本来防止super block自身损坏)
  • 空闲块:inode, 间接块,以inode为参考, 修改inode bitmap达到一致, 所有看起来在用的inode都会有bitmap标记
  • inode状态:如果inode的字段不合法,认为不易修复,删掉这个inode
  • inode链接:扫描整个目录树,重新计算引用计数,如果找到已经分配的inode但没有目录引用,放到lost+found
  • 重复和坏块,清除不正确的指针
  • 目录检查:确保无环,目录中每个inode已分配等

磁盘清理工具:重排inode的data block来减少碎片

fsck 存在的一个问题:很复杂

fsck 最关键的问题:太慢了!

另外的方法:WAL

Linux ext34, Windows NTFS 采用的方法: 加上journal block

super | **journal** | group0 | group1 | …

journal中条目的形式:

TxB | inode | bitmap | data | TxE (物理日志,也有逻辑日志的做法,可能提高性能,节省空间)

checkpoint: 成功写入journal之后,就是一个checkpoint

先写WAL, 再写文件系统元数据,最后落盘data

问题:写日志的时候崩溃?

问题发生在,一条日志(以物理为例)可能太大,以至于不能被原子写入(常态)

  • 日志内的数据可以被磁盘调度为小块乱序写入
  • 写入的部分错误难以检查,例如在写入data段的时候出错,但其他部分正确(但也可以checksum? 这就是ext4的一项重要更新,通过在TxB和TxE之中包含checksum来加快写入速度)
  • 磁盘的写缓冲,早期OS强制写入顺序就是通过关中断实现的: 写A,关中断,写B;有缓冲的状态下,写入缓冲就会返回。还要保证顺序的话,一种是禁用写缓冲,更现代的方法是写屏障(有趣的是一些磁盘产商忽略了写屏障,即使存在错误的风险,“快速几乎总是打败慢速,即使快速是错的”
  • 所以早期的方案是这样的,先写除了TxE之外的所有块,这部分出错这个事务就是未提交的,会被重置;然后第二步再写TxE, 磁盘保证写512字节的block的原子性,因而TxE是原子的。流程称为日志写入,日志提交,加检查点

此时,恢复仅仅是重放(replay)

批处理:

和TLB缓存很像,fs可以为了性能将磁盘写入合并批处理,在内存之中标记dirty

日志长度有限:循环,checkpoint之后前面的日志可以释放掉再重新写入

物理日志严重的写放大:元数据日志(linux ext3的另一个mode, windows NTFS),WAL之中不保存data段,只保存inode, inode bitmap,block bitmap等元数据修改。此时需要先写data, 来避免指向垃圾数据

  • 这样的“强制先写入被指向的对象,再写入指针”是崩溃一致性的核心

棘手的问题:块复用,感觉有点绕,总之是删除+文件夹这种递归结构+只记录元数据+重用得到了一些不好的结果,linux ext3的解决方法是删除也有revoke日志

其他崩溃一致性的解法:

  • COW
  • 反向指针,在被引用块里面添加引用块的引用(惰性崩溃一致性)
  • 软更新,排序所有写入,复杂
  • 乐观崩溃一致,例如校验和的方案

ZFS使用COW和日志

chapter43 LFS 日志文件系统

很像LSM,同样是为了利用磁盘的顺序写,同时只做顺序写,读时读最新版本,GC回收旧版本数据

LFS将每次文件系统的更新都顺序写入(data, inode),并以写缓冲实现批量写的性能提高,带来一个问题是不知道inode在哪里了

因而引入了一个中间层imap,记录inode, addr的对,imap需要保证持久,每次写入inode时,imap进行更新

imap放在哪里?如果在固定段,由于它更新的频繁性,所以需要非常多的磁盘寻道,不可接受

所以把imap和inode一起,写到更新后面

那去哪里找imap呢?(有点像 一级一级的索引,现在要找索引入口)

在磁盘的固定处维护检查点区域CR, CR指向最新imap,而CR的性能可以通过降低更新频率,例如每30s定时更新来解决,inode的更新全放到imap了

imap还解决了递归更新的问题

读取的时候有内存缓存,直接读里面的imap就行

怎么做GC? 在data block的开头有对inode块的反向引用+自身在inode中偏移量T,称为segment summary block

所以data → segment → 查最新的imap得到inode addr → inode[T] == data ? alive : dead

更简单的空间换时间的方法,在imap里面记录版本号,在segment里面也记录版本号

清理哪些段?一种粗略的方法是冷热分离

经常覆盖内容的段是热段,尽可能保留;反之冷段可以早清

崩溃恢复

  • 写CR崩溃:1. 给cr做一个副本 2.更新cr时,通过 时间戳 + CR + 时间戳的方式,检测更新的一致性,时间戳是原子的
  • 其他崩溃:CR后的数据丢了,假设CR是30s, 那不超过30s, 当然可以在CR里面包含更多信息,从而在最后一个检查点之后,尽量找到日志的结尾,并尽可能恢复有效的段,称为前滚 roll forward

LFS的思想在ZFS, Linux btrfs等有所体现,通过快照来得到fs的版本化

chapter 44 检测错误

磁盘错误:“有声的” 和“无声的”

  • 前者例如意外的不可读,数据位反转,可以被ECC修复或检错的,总之会被磁盘发现
  • 后者如写歪了

前者的解决方法是各种方式的RAID

后者的解决方法是校验和,xor, add, fletcher, crc

像“写歪了”(addr x写到y, 磁盘A写到B)这种如何检测?在校验和之中加入块的磁盘和扇区号这样的物理标识符

还有一个特殊的情况是写入丢失,解决方法有写入后检验,或者在inode上加校验和(这样inode + data block全写丢了才会发现不了)

何时检测:定时,磁盘的位是会衰退的

system-design-interview笔记

· 27 min read
ayanami

限流器

  • 放在哪里?

    • 客户端, 容易被伪造绕过
    • 服务端应用代码, 灵活性高, 但占据工程资源
    • 云上微服务, api网关等, 灵活性低
  • 经典算法:

    • 令牌桶: 固定速率产生令牌, 每个请求消耗令牌

      • pros: 容易实现, 内存占用少, 允许突发流量
      • cons: 调参困难, 可能需要不断填充桶
    • 漏桶: 请求进入一个定长FIFO队列, server给定速度从队列取数据处理

      • pros: 容易实现, 内存占用少, 请求以固定速率处理
      • cons: 突发请求占据队列使得新请求得不到处理, 调参困难,可能需要不断填充桶
    • 固定窗口计数器: 每一个给定时间窗口进行计数,例如每分钟100个请求

      • pros: 容易实现, 内存占用少
      • cons: 突发请求可以达到两倍限制的QPS(在窗口边缘有突刺)
    • 滑动窗口日志: 记录请求时间戳, 数据保存在redis等缓存,每当新请求进来的时候把过时时间戳删除, 如果请求时间戳比时间窗口的最低值还低, 拒绝请求

      • pros: 速率限制准确
      • cons: 内存开销大, 需要存储多个时间戳
    • 滑动窗口计数器: 某时刻限制请求数 = 窗口上限 - 上一个窗口请求数 * 这一个窗口的和上一个窗口的重叠百分比。例如, 窗口为每分钟1000, 上一分钟800, 在这一分钟的30秒时, 限制这一分钟窗口的请求数最多为 1000 - 30 / 60 * 800 = 600

      • 相当于假设上一个窗口的平均速率

      • pros: 平滑了流量峰值, 内存高效(只需要计数)

      • cons: 不太严格, 然而其实可以

        然而,这个问题可能并不像它看起来那么糟糕。 根据Cloudflare[10]所做的实验,在4亿个请求中,只有0.003%的请求被错误地允许或限制速率

超过速率限制

如果一个请求被限制了速率,API会向客户端返回一个HTTP响应代码429(请求太多)。根据不同的使用情况,我们可能会将速率受限的请求排队等候以后处理。 例如,如果一些订单由于系统过载而受到速率限制,我们可以保留这些订单以便以后处理。

限流器请求头

一个客户如何知道它是否被节流?客户端如何知道在被节流之前允许的剩余请求的数量?答案就在HTTP响应头中。限流器向客户端返回以下 HTTP 标头:

  • X-Ratelimit-Remaining:窗口内允许请求的剩余数量
  • X-Ratelimit-limit:它表示客户端在每个时间窗口可以进行多少次调用
  • X-Ratelimit-Retry-After:等待的秒数,直到你可以再次提出请求而不被节流。

当用户发送过多请求时,将向客户端返回 429 too many requests 错误和 X-Ratelimit-Retry-After 标头。

  • 规则被存储在磁盘上。工作者经常从磁盘中提取规则,并将其存储在高速缓存中。
  • 当客户端向服务器发送请求时,该请求首先被发送到限流中间件。
  • 限流中间件从缓存中加载规则。它从Redis缓存中获取计数器和最后一次请求的时间戳。根据响应,限流器决定:
    • 如果请求没有速率限制,它将被转发到API服务器。
    • 如果请求受到速率限制,限流器会向客户端返回 429 too many requests 错误。 同时,请求被丢弃或转发到队列。

分布式限流

redis:

  • lua脚本
  • sorted set每个用户有一个自己的set,试图执行动作时,使用MULTI原子地执行下列操作: ZREMRANGEBYSCORE 删除给定时间间隔前的元素, ZADD 添加当前时间戳, ttl设置为rate limit,计算sorted set的数量,如果超过限额,就失败 (滑动窗口日志)(有一说一我觉得这个如果没有精确的需求不如滑动窗口计数器, 开销感觉有点大)

多限流器同步: 用户hash redis集群分配之类, 尽量别做这种事情

其他层级的限流:

  • iptables
  • ...

一致性hash

基本步骤如下:

  1. 使用均匀分布的哈希函数将服务器和键映射到环上。
  2. 要想知道一个键被映射到哪个服务器,从键的位置顺时针查找,直到找到环上的第一个服务器。

步骤是很简单的, 麻烦的是问题

环上服务器分区大小难以保证相同(添加删除服务器)

解决方法: 虚拟节点(分成更小的块)

每个服务器动态地分配小块(虚拟节点), 这样还可以考虑服务器容量自动缩放问题

一致性模型

N = 副本数

W = 大小为 W 的规定写入。要将写入操作视为成功,必须从 W 个副本确认写入操作。

R = 大小为 R 的读取规定人数。为了使读取操作被认为是成功的,读取操作必须等待至少R个副本的响应。

如何配置N、W和R以适应我们的使用情况?

下面是一些可能的设置:

  • 如果R=1,W=N,系统被优化为快速读取
  • 如果W=1,R=N,系统被优化为快速写入
  • 如果W+R>N,就可以保证强一致性(通常N=3,W=R=2)。
  • 如果W+R<=N,则不能保证强一致性

复制->高可用->不一致

发现并解决冲突: 常用是向量时钟 vector clock, 但有几个问题

  • 处理冲突逻辑复杂
  • 动态增加删除服务器逻辑复杂

参考:

实际业界的用例参考Dynamo

算法简介:在每个服务器内部维护一个所有服务器的vector

  • 当自己发生事件时,增加vector[self], 并告诉需要告诉的服务器(核心就在于不广播, 没有全局时间!当然也因此不保证解决冲突)
  • 当自己收到A server的事件时, vector[A] += d

最后想要觉得一个事件的全局顺序的时候, 查看所有的server的vec

如果有一个server的vec是最大的,那么严格有因果关系,取它的值就行

但如果没有最大的vec, 就需要解决冲突的策略

策略

  1. 交给客户端, 如dynamo
  2. 加上时间戳, vec' = [...servers, timestamp]冲突取ts最大的
  3. 随机取一个

所以向量时钟算法的实质是:

(1)将逻辑上可以合并的冲突成功合并;

(2)逻辑上无法合并的冲突依旧冲突;

拓展: 向量时钟的剪枝, riak, 牺牲绝对的正确性(false merge)来换取对太长的vec clock的剪枝

gossip 协议 quorom

唯一ID生成器

  • 多主复制: 下一个id += k, k是服务器数量,拓展性差

  • uuid

    • 引自维基百科,"在每秒产生10亿个UUIDs,大约100年后,创造一个重复的概率会达到50%"。

    • 优点:

      • 生成 UUID 很简单。服务器之间不需要协调,因此不会有任何同步问题。

      • 该系统易于扩展,因为每个 Web 服务器负责生成它们使用的 ID。 ID 生成器可以轻松地与 Web 服务器一起扩展。

    • 缺点:

      • ID 是 128 位长, 空间开销。
      • ID 不会随时间上升
      • ID 可以是非数字的
  • ticket服务器, 在分布式系统之中维持一个唯一的ticket server用于签发id

    • **优点:**数字 ID,易于实施,适用于中小型应用程序

    • **缺点:**单点故障

  • twitter雪花算法

    • 符号位:1 位,它将始终为 0。这是为将来使用保留的。它可以潜在地用于区分有符号数和无符号数。
    • 时间戳:41 位。自纪元或自定义纪元以来的毫秒数。我们使用 Twitter 雪花默认纪元 1288834974657,相当于 2010 年 11 月 4 日 01:42:54 UTC。
    • 数据中心 ID:5 位,这给了我们 25=3225=32 个数据中心。
    • 机器 ID:5 位,每个数据中心有 25=3225=32 台机器
    • 序列号:12 位对于在该机器/进程上生成的每个 ID,序列号都会递增 1。该数字每毫秒重置为 0。

也就是说雪花在一台机器上1毫秒可以支持4096个新id

额外要点:

  • 时钟同步。在我们的设计中,我们假设 ID 生成服务器具有相同的时钟。当服务器在多个内核上运行时,此假设可能不成立。同样的挑战存在于多机场景中。时钟同步的解决方案超出了本书的范围;但是,了解问题的存在很重要。网络时间协议是这个问题最流行的解决方案。
  • 节段长度调整。例如,较少的序列号但较多的时间戳位对低并发性和长期应用是有效的。
  • 高可用性。由于 ID 生成器是关键任务系统,因此它必须具有高可用性

拓展阅读: 网络时间协议

短URL

点击较短的别名重定向到原始url

  • URL缩短:给定一个长的URL => 返回一个短得多的URL

  • URL重定向:给定一个短的URL => 重定向到原来的URL

  • 高可用性、可扩展性和容错考虑

值得在这里讨论的一件事是 301 重定向与 302 重定向。

  • 301重定向。301重定向表明,请求的URL被 "永久 "地移到了长URL上。由于是永久重定向,浏览器会缓存响应,对同一URL的后续请求将不会被发送到URL缩短服务上。相反,请求将直接被重定向到长网址服务器。

  • 302重定向。302重定向意味着URL被 "暂时 "移到长URL上,这意味着对同一URL的后续请求将首先被发送到URL缩短服务上。然后,它们会被重定向到长网址服务器。

每种重定向方法都有其优点和缺点。如果优先考虑减少服务器负载,使用301重定向是有意义的,因为只有同一URL的第一个请求被发送到URL缩短服务器。然而,如果分析是重要的,302重定向是一个更好的选择,因为它可以更容易地跟踪点击率和点击的来源。

一个简单的解决方案 <shortURL, LongURL>的rdbms

假设系统支持3650亿个url=10年 * 每天1亿

可以使用0-9a-zA-Z62个字符, 62^n > 3650 亿 n = 7

方法1: hash+碰撞解决

longURL -> hash -> short -> exist on db?(opt bloomfilter + query) -> save/return/collision

collision 的一种解决方法是 longURL -> longURL + predefined string

方法2: base62转换

给每一个请求的长url分配一个数字类型的唯一id, 再对唯一id做base62转换

坏处是可能出现安全问题

longURL -> in DB ? -> yes,return short
|
-> no, generate new ID -> id to base62 -> save id, longurl, shorturl in db

读多于写, 加缓存

爬虫系统

算法侧: 从url seed开始, 将link视为边, 用BFS去爬取不同的网页

架构侧:

seed url -> url frontier -> html downloader(DNS resolver) -> content parser -> content seen(重复检测器, 例如 hash, BF) + 存储-> link extractor -> url filter -> url seen -> url storage

优先级: 简单的做法是根据Url内容变化的速度(可以基于历史抓取记录)和本身的价值(例如是个人博客还是官方网站)设计一个优先级估价函数, 根据url区分k个工作队列, 保证一个队列内只有一个url(的多个请求), 这样就可以实现优先级

爬虫需要考虑礼貌性, DDoS

做法是每个url作为一个后端队列, 在优先级的selector出来之后, 维护一张(url,back queue)的表, 将url放到back queue里面去

对每一个url(backqueue), 维护一个时间t, 是下一次抓取的最早时间(例如当前时间+10倍前一次获取时间)

而爬虫线程每次从时间的最小堆之中取出元素, (如需要, 等待到t), 然后爬取

html下载器: Robots 排除协议, 分布式抓取, 超时

其他问题: 冗余内容, 蜘蛛陷阱, 垃圾数据, js-需要动态渲染得到链接和其他数据

通知系统

删重, 跟踪, 限速, 用户设置(接受哪些通知), 失败时的重试机制, 安全性(客户验证)

推送流:

核心在fan out系统, 读扇出(拉模式)还是写扇出(推模式), 热键问题和速率问题

混合: 对大多数用户使用推送模式。对于名人或有很多朋友/粉丝的用户,我们让粉丝按需提取信息内容以避免系统过载

get friend IDs: graph DB

fanout service先从graph DB拿到friend ids, 再从用户缓存(用户db)得到朋友相关信息:

例如,如果你把某人调成静音,她的帖子将不会显示在你的信息流中,尽管你们仍然是朋友。帖子可能不显示的另一个原因是,用户可以有选择地与特定的朋友分享信息或对其他人隐藏信息。

把更新的需求包成任务(例如<post_id, user_id>)丢到mq

mq入库, 写缓存

缓存层级:

  • News Feed:它存储了信息的ID。

  • Content:它存储每个帖子的数据。受欢迎的内容被存储在热缓存中。

  • Social Graph:它存储用户关系数据。

  • Action:它存储有关用户是否喜欢帖子、回复帖子或对帖子执行其他操作的信息。

  • Counters:它存储点赞、回复、关注者、关注等的计数器。

聊天系统

无状态有状态分离

  • 无状态的服务 http

    无状态服务是传统的面向公众的请求/响应服务,用于管理登录、注册、用户资料等。这些是许多网站和应用程序中的常见功能。

    无状态服务位于负载均衡器后面,其工作是根据请求路径将请求路由到正确的服务。这些服务可以是单体的,也可以是单独的微服务。我们不需要自己建立许多这样的无状态服务,因为市场上有一些服务可以很容易地被集成。

    我们将深入讨论的一个服务是服务发现。它的主要工作是给客户提供一个客户可以连接到的聊天服务器的DNS主机名列表。

  • 有状态的服务 websocket

    唯一有状态的服务是聊天服务。该服务是有状态的,因为每个客户都与一个聊天服务器保持持久的网络连接。在这个服务中,只要服务器仍然可用,客户通常不会切换到另一个聊天服务器。服务发现与聊天服务密切协调,以避免服务器过载。我们将在深入研究中详细介绍。

服务器的切分:

  • chat server 管理信息的收发
  • presence server 管理在线/离线状态
  • api server 处理无状态服务
  • notification server 推送通知

DB选择: KV数据库

为什么?聊天数据的读写模式

  • 数据量巨大 需要水平拓展
  • 只有最近的聊天记录才会被频繁访问
  • 但“最近”里面也不完全是顺序的, 引用, 跳转, 提及等
  • 读写比约为1:1, 读并不远远高于写

这样的wkld下kv比关系型的优势:

  • 水平拓展轻松
  • 分层架构对热数据容易优化
  • 关系型数据库的index在数据量变大时昂贵

消息ID设计:

一对一聊天: 主键message id

群聊: 复合主键 {channel_id, message_id}

问题: id用什么?

要求: 唯一性 + 可以按照时间排序

  • 自增: 分布式实现困难
  • 全局序列号发生器: 将时间项提前就可以按照时间排序
  • 本地序列号生成器: 只保证消息在一个组内(channel内)唯一, 实现比较简单

发送信息的流程

  1. 用户A向聊天服务器1发送了一条聊天信息。
  2. 聊天服务器1从ID生成器获得一个信息ID。
  3. 聊天服务器1将消息发送至消息同步队列。
  4. 消息被储存在一个键值存储中。
  5. a. 如果用户B在线,信息被转发到用户B所连接的聊天服务器2。
  6. b. 如果用户B处于离线状态,则从推送通知(PN)服务器发送推送通知。
  7. 聊天服务器2将消息转发给用户B,用户B和聊天服务器2之间有一个持久的WebSocket连接。

群组聊天:

第一种方法: A发消息, 在每一个成员的收件箱里面复制一个副本, 适用于群组人数较少的时候(例如微信的500人约束); 每个收件人有自己的收件箱(消息同步队列), 所以并不保证一致性

在线状态指示器:

  • naive的做法: 建立连接就在线, 断开就离线。问题在网络波动时候, 变化太快
  • 更优雅的做法, 心跳
  • 推送, 类似微信这种小群(500人限制)可以直接查询实时的ws连接。大群需要懒加载

扩展聊天应用程序以支持媒体文件,如照片和视频。媒体文件的大小明显大于文本。压缩、云存储和缩略图是值得讨论的话题。

  • 端到端加密。Whatsapp支持信息的端到端加密。只有发件人和收件人可以阅读信息。有兴趣的读者请参考参考资料中的文章[9]。
  • 在客户端缓存信息,可以有效地减少客户端和服务器之间的数据传输。
  • 提高加载时间。Slack建立了一个地理分布的网络来缓存用户的数据、频道等,以获得更好的加载时间[10]。
  • 故障处理。
    • 聊天服务器错误。可能有数十万,甚至更多的,坚持不懈的连接到一个聊天服务器。如果一个聊天服务器离线,服务发现(Zookeeper)会提供一个新的聊天服务器,让客户建立新的连接。
    • 消息重发机制。重试和排队是重发消息的常用技术。

多媒体支持的大体流程

  • client 通过 rest 将多媒体资源发送到服务器
  • 服务器转换媒体文件(例如图片生成缩略图, 压缩)
  • 服务器存到s3
  • 服务器通过s3链接响应client
  • 客户端再将s3链接通过ws发送(广播)给聊天的其他用户
  • 其他client 收到s3连接, 根据定义的消息类型进行渲染

简单的搜索支持: trie

topK: 先找到前缀, 再遍历子树

问题: 太慢

解决方法:

  • 在每个节点缓存常用的前k个查询
  • 控制前缀的最大长度

更新trie: 数据搜集, 对于实时应用服务, 短时间间隔; 对于非实时的, 可以例如一周定时更新一次

合法性: 自动完成可以根据hash等过一个filter, 避免补出禁止的网站等

多语言:unicode trie

分片: 可以基于字母, 但是要考虑到频率问题

实时(趋势)搜索: 流式, 领域特定, AI

视频流

核心是分成几个部分, 一部分类似传统的server, 提供视频metadata

另一部分依托云服务和CDN, 做好视频传输和解码编码

其中特定部分的逻辑复杂, 例如解编码的模块化和引擎化, CDN贵所以视频基于历史数据做冷热分离, 冷视频走自建而不是CDN

还有比如错误处理, 数字版权之类

具体好多细节

临近服务

由于是读远多于写的情况, 所以常见用关系型db

关系型的问题是, 如果是经纬度, 二维数据index利用率低

解决方法是二维转一维再index搜索, 例如geohash, R tree, 四叉树, google s2

geohash: 经纬度网格编码再转base32, 共享前缀越长, 越接近

边界问题: 反过来不成立, 接近的两个地块可能共享前缀并不长(在子午线/赤道等大格子边界), 所以geohash LIKE 'sth%'检索出来结果不全

方法是不仅检索自己格子的geohash, 也把邻居格子的一起检索

业务问题:范围内部商家不够, 解决方法是放大格子继续检索

google s2: 基于希尔伯特曲线的球面->1d算法, 保证2d上接近在1d上也接近

JUC

· 11 min read
ayanami

自旋锁->自旋N次(N自适应, 取决于先前历史,当前负载等)->升级为重量级锁

重量级, mutex 本质上的syscall, 轻量级, CAS去尝试拿到对象头中的锁标识字节MarkWord

更新成功说明没人抢

偏向锁: 当某个线程第一次获取锁时, 接下来都没有其他线程拿, 那这个线程后续拿锁就连CAS也不需要

无锁->偏向锁->自旋锁->重量级锁

JMM

所有可能出现竞争的变量(成员, 静态等)均在主内存

局部变量线程私有, 工作内存相互隔离, 只能通过主内存同步

volatile

需要立即看到修改的值, 每一次读取都从主线程读, 每一次写都把工作内存的值刷新到主线程

cs144 labs(Winter 2024)

· 31 min read
ayanami

Lab0

这个lab纯纯的热身lab, part1是用webget简单进行个请求, 类似csapp的网络lab part1

part2字符串操作, 恶心一点的就是string_view的peek和一个ring buffer不太能兼容, 总之我的代码效率也挺低的就不拿出来献丑了()

Lab1

这个lab要求实现tcp字节流抽象的重组部分Reassembler

调的时候还是很恶心的, 非常多的edge case, 建议好好看测试是怎么构造的

写的时间最久的一个lab, 但这里笔记没有多少, 因为Lab1结束到Lab2开始的两个星期干别的去记不清当时的感受了()

还是放个源代码

#include "reassembler.hh"

NJU操作系统(jyy OS)课程笔记-并发部分

· 17 min read
ayanami

lec5 多处理器编程:

理解程序:状态机模型,我们把一个程序看成一个状态机,程序的状态是{寄存器,内存},而每次取出一条指令、再执行的过程的就是状态迁移到过程。

由这个状态机模型,我们可以有非常多的 trick,例如 debug 单步执行,例如模拟器,例如如果某些指令是“可逆”的,就可以在 debug 的时候反向执行,“时间倒流”(gdb 也提供了这一模式)……

对于并发程序,多处理器模型,我们的直觉告诉我们,可以把这个程序看成是多个状态机,并发的过程就是每次取出一个状态机,执行一步,而所有的状态机有共享的内存(线程模型)…… 这样的模型已经足够复杂,状态数是指数增长的,解决需要考虑所有状态的问题是 NP 完全的。

danger

但更重量级的是,这样的模型是错的!

并发编程的问题:

  1. load/store 的非原子性
  2. 编译器的优化

nginx基础

· 6 min read
ayanami

配置文件

server 虚拟主机, 根据 {listen, server_name}IP-port或者domain-port对来进行唯一性标识, 可以单项相同, 会匹配另一项转发

location + root, index指定主页等

配置静态页面结束

反向代理: server端请求转发

server {
listen 8001;
server_name any_name;
location / {

CS144 Lecture Notes

· 69 min read
ayanami

Unit 1: Internet and IP

网络四层模型:application, transport, network, link

7层OSI, 拆app,trans,link为更细的两个

网络的基础和底层是IP, 只有IP model是不可替代的“细腰”,上层的application、transport和下层的link都是可以替换的

application 应用层,smtp, ftp, http, ssh

transport 传输层,tcp, udp, rdp

link 连接层,4G, WiFi, ...

网络连接的几种常见例子:

client-server, BitTorren 一服务器多client的集群, ......

网络 application上 :read/write