介绍

BlackCloud 的博客

Powered by mdbook

Benchmarks

记录一下经常用到的算法/容器的 Benchmark (主要使用 C++)

随用随测, 可能不会特别准确, 工作侧重于在特定负载下选择最好的算法/容器, 所以主要记录不同选择的相对性能

String Hash

  • 4-16 个字符的短字符串
|               ns/op |                op/s |    err% |          ins/op |         bra/op |   miss% |     total | benchmark
|--------------------:|--------------------:|--------:|----------------:|---------------:|--------:|----------:|:----------
|       11,094,427.00 |               90.14 |    0.4% |  111,833,763.00 |  12,383,248.00 |    4.0% |      0.12 | `CRC32`
|        8,181,207.00 |              122.23 |    3.2% |   81,658,267.00 |  12,940,438.00 |    3.8% |      0.09 | `FNV`
|        5,397,684.00 |              185.26 |    0.4% |   56,646,296.00 |   9,432,868.00 |    5.3% |      0.06 | `Murmur2`
|        6,749,671.00 |              148.16 |    1.2% |   67,046,257.00 |   9,432,870.00 |    5.3% |      0.08 | `Murmur3_x86_32`
|       11,833,767.00 |               84.50 |    0.5% |  128,028,092.00 |   5,949,857.00 |    8.5% |      0.13 | `Murmur3_x86_128`
|        7,636,166.00 |              130.96 |    0.8% |   91,747,270.00 |   6,441,582.00 |    7.7% |      0.09 | `Murmur3_x64_128`
  • ~200 个字符的中等长度字符串
|               ns/op |                op/s |    err% |          ins/op |         bra/op |   miss% |     total | benchmark
|--------------------:|--------------------:|--------:|----------------:|---------------:|--------:|----------:|:----------
|      134,931,464.00 |                7.41 |    0.0% |1,235,797,487.00 |  80,973,317.00 |    1.2% |      1.48 | `CRC32`
|      154,305,226.00 |                6.48 |    0.0% |1,250,489,432.00 | 179,927,192.00 |    0.6% |      1.70 | `FNV`
|       41,743,783.00 |               23.96 |    0.0% |  474,381,552.00 |  50,949,403.00 |    1.8% |      0.46 | `Murmur2`
|       44,541,974.00 |               22.45 |    0.1% |  446,744,071.00 |  51,629,173.00 |    1.8% |      0.49 | `Murmur3_x86_32`
|       44,402,780.00 |               22.52 |    0.1% |  461,885,844.00 |  16,677,849.00 |    5.3% |      0.49 | `Murmur3_x86_128`
|       26,934,988.00 |               37.13 |    0.2% |  307,733,470.00 |  16,584,743.00 |    5.3% |      0.30 | `Murmur3_x64_128`

Map

  • 完全随机的 int key, 做 find_or_insert 操作
|               ns/op |                op/s |    err% |          ins/op |         bra/op |   miss% |     total | benchmark
|--------------------:|--------------------:|--------:|----------------:|---------------:|--------:|----------:|:----------
|       35,153,700.00 |               28.45 |    1.8% |  157,273,534.00 |  33,488,895.00 |    1.5% |      0.41 | `unordered_map:random`
|      136,277,935.00 |                7.34 |    1.7% |  193,896,493.00 |  48,073,497.00 |   13.1% |      1.49 | `map:random`
|       58,396,319.00 |               17.12 |    0.8% |  312,029,741.00 |  92,169,201.00 |    4.9% |      0.66 | `btree_map:random`
|       12,382,928.00 |               80.76 |    0.8% |   92,646,655.00 |  12,036,977.00 |    5.7% |      0.14 | `robin_hood_map:random`
|       17,489,723.00 |               57.18 |    1.2% |   90,160,012.00 |  17,922,242.00 |    4.4% |      0.20 | `dense_hash_map:random`
|          810,089.00 |            1,234.43 |    3.2% |    3,600,629.00 |     300,147.00 |    0.0% |      0.01 | `array(test):random`
  • 顺序出现的 int key, 每个 key 重复出现 32 次, 组与组之间略微打乱(比如 0 0 0 1 0 1 1 2 1 2 2 2 ... 这样的序列), 做 find_or_insert 操作
|               ns/op |                op/s |    err% |          ins/op |         bra/op |   miss% |     total | benchmark
|--------------------:|--------------------:|--------:|----------------:|---------------:|--------:|----------:|:----------
|       19,428,866.00 |               51.47 |    1.2% |  137,639,829.00 |  26,979,609.00 |    1.2% |      0.22 | `unordered_map:sequential`
|      305,547,283.00 |                3.27 |    0.5% |  494,128,064.00 | 130,782,008.00 |   17.3% |      3.34 | `map:sequential`
|      183,810,512.00 |                5.44 |    0.4% |  973,906,327.00 | 302,459,238.00 |    4.8% |      2.03 | `btree_map:sequential`
|       34,349,617.00 |               29.11 |    0.3% |  183,005,851.00 |  24,127,612.00 |   12.6% |      0.38 | `robin_hood_map:sequential`
|       26,675,597.00 |               37.49 |    1.2% |  185,300,938.00 |  46,039,048.00 |    5.1% |      0.30 | `dense_hash_map:sequential`
|        6,950,046.00 |              143.88 |    0.8% |   38,400,593.00 |   3,200,143.00 |    0.0% |      0.08 | `array(test):sequential`

Idioms

1. CRTP

我想实现如下伪代码所示的编程模式, 其中 Host 代表 C/S 模型中的任何一个端点, ServerClient 都需要处理收到的包, 但对于每个类型的包, 它们有不同的处理方式. 收包的逻辑是相同的, 因此我希望复用 handle_packet. 最简单的方法就是用虚函数

class Host {
    void handle_packet(...) {
        // do receive packet
        switch (packet_type) {
        case (a):
            handle_packet_a();
            break;
        case (b):
            handle_packet_b();
            break;
        };
    };
    virtual void handle_packet_a();
    virtual void handle_packet_b();
};

class Server: public Host {
    virtual void handle_packet_a() override;
    virtual void handle_packet_b() override;
};

class Client: public Host {
    virtual void handle_packet_a() override;
    virtual void handle_packet_b() override;
};

但这样设计只是为了复用 handle_packet 的逻辑, 而没有动态绑定的需求, 因此引入虚函数增加了没有必要的开销

这种编译期多态可以通过 CRTP 实现

template<typename Derived>
class Host {
    void handle_packet() {
        switch (packet_type) {
        case (a):
            static_cast<Derived&>(*this).handle_packet_a();
            break;
        case (b):
            static_cast<Derived&>(*this).handle_packet_b();
            break;
        };
    }
};

class Server: public Host<Server> {
    void handle_packet_a();
    void handle_packet_b();
};
// ...

这样可以完全去除虚函数

2. SFINAE

Substitution Failure Is Not An Error

起因是想在用 CRTP 时实现编译时检测子类是否具有某个方法, 仅当有方法时才调用的需求, 例如:

template<typename D>
class Animal {
    void live() {
        while (true) {
            static_cast<D&>(*this).eat();
            static_cast<D&>(*this).sleep();
            static_cast<D&>(*this).bark(); // compile error! Cat will never bark
            // Want: if constexpr (D has member `bark`) then call `bark`
        }
    }
};

class Cat: public Animal<Cat> {
    void eat();
    void sleep();
};

class Dog: public Animal<Dog> {
    void eat();
    void sleep();
    void bark();
};

这种在编译期模板实例化时确定模板参数是否有某种性质的行为被称为内省(introspection)

包含模板的重载函数的候选集中的某些(或者全部)候选函数来自 模板实参替换模板形参 的模板实例化结果, 在这个过程中, 某些模板的实参替换可能会失败, 这种替换失败(Substitution Failure)并不会立即被当作编译错误(Error)抛出, 这个替换失败的模板会被从候选集中删除, 只要到最后存在成功的替换, 即重载函数候选集不为空, 则这个重载函数的解析就是成功的, 编译也能通过

见来自 替换失败并非错误 的例子:

struct Test {
  typedef int foo;
};

template <typename T>
void f(typename T::foo) {}  // Definition #1

template <typename T>
void f(T) {}  // Definition #2

int main() {
  f<Test>(10);  // Call #1.
  f<int>(10);   // Call #2. 并无编译错误(即使没有 int::foo)
                // thanks to SFINAE.
}

在编译时, f<Test>(10) 会针对 f 的两个定义做两次 Substitution

  1. 第一次替换得到的函数定义是 void f(typename Test::foo) {}
  2. 第二次替换得到的函数定义是 void f(Test) {} 因此这个调用拥有两个可能的候选, 而根据实参 10 的类型可以推导得到只有 1. 符合要求, 因此最终会调用 1., 编译通过

f<int>(10) 也会针对 f 的两个定义做两次 Substitution

  1. 得到 void f(typename int::foo) {}, 但 int::foo 并不存在, 因此这个替换失败了, 这个函数并不会进入候选集
  2. 得到 void f(int) {} 因此这个调用拥有一个可能的候选, 而实参 10 的类型可以匹配这个唯一的候选, 因此最终会调用 2., 编译通过

上面的例子里, SFINAE 恰好干了在开头时我想干的事: 在编译时判断一个成员是否存在于 struct/class 中.

经过修改可以得到以下真正实现了这个需求的代码, 检查类型 T 上是否有拥有 bark 成员函数:

template<typename T>
struct has_member_bark {
    private:
        template<typename U> static auto check(int) 
            -> decltype(std::declval<U>().bark(), std::true_type());
        template<typename U> static std::false_type check(...);
    public:
        enum {value = std::is_same<decltype(check<T>(0)), std::true_type>::value};
};

// 接上面 Animal 的例子...
if constexpr (has_member_bark<D>::value) {
    static_cast<D&>(*this).bark();
}

上述例子工作的原理是

  1. 编译时要对 has_member_bark<T>::value 求值, 其值等于 std::is_same<decltype(check<T>(0)), std::true_type>::value

  2. 需要推导 decltype(check<T>(0))

  3. check<T>(0) 有两个可选的模板

    • template<typename U> static auto check(int) -> decltype(std::declval<U>().bark(), std::true_type())
    • template<typename U> static std::false_type check(...)

    其中后者由于指定了 ... 作为参数, 因此拥有最低的匹配优先级, 所以编译器会优先尝试第一个定义

    第一个定义的返回值需要被推导, 其类型为

    decltype(std::declval<U>().bark(), std::true_type())
    

    decltype 内是一个 comma expr, 其值等于最后一个表达式的值, 但是求值是从前到后进行的, 因此必须先推导 std::declval<U>().bark() 的类型, 这时

    • 如果 U::bark 不存在, 则这个替换就会失败, 因此 check 的第一个模板就会被删除, 只留下第二个, 则 decltype(check<T>(0))false_type
    • 如果 U::bark 存在, 则这个替换成功, check 的第一个模板成为最终的选择, decltype(check<T>(0))true_type
  4. 无论 T::bark 是否存在, 由于 SFINAE, 最终总有一个 check 被匹配, 如果 T::bark 存在, 则 value 最终为 true_type, 否则是 false_type

一个完整的可编译的例子:

#include <iostream>

template<typename T>
struct has_member_bark {
    private:
        template<typename U> static auto check(int) 
            -> decltype(std::declval<U>().bark(), std::true_type());
        template<typename U> static std::false_type check(...);
    public:
        enum {value = std::is_same<decltype(check<T>(0)), std::true_type>::value};
};

template<typename D>
class Animal {
public:
    void live() {
        // while (true) {
            static_cast<D&>(*this).eat();
            static_cast<D&>(*this).sleep();
            if constexpr (has_member_bark<D>::value) {
                static_cast<D&>(*this).bark();   
            }
        // }
    }
};

class Cat: public Animal<Cat> {
public:
    void eat() {
        std::cout << "Cat eat" << std::endl;
    }
    void sleep() {
        std::cout << "Cat sleep" << std::endl;
    }
};

class Dog: public Animal<Dog> {
public:
    void eat() {
        std::cout << "Dog eat" << std::endl;
    }
    void sleep() {
        std::cout << "Dog sleep" << std::endl;
    }
    void bark() {
        std::cout << "Woof!" << std::endl;
    }
};

int main() {
    Cat().live();
    Dog().live();
    return 0;
}

输出

Cat eat
Cat sleep
Dog eat
Dog sleep
Woof!

3. PIMPL

Pointer to IMPLementation

写了一个库, 起初供用户 include 的头文件里有这样的声明:

class Server {
    public:
        Server();
        void start();
    private:
        void receive_packets();
        void handle_packet();
        void handle_packet_data();
        void handle_packet_ping();
        void handle_packet_pong();
    private:
        int socket;
        // more members ...
};

但首先, 暴露给用户的接口只有 start, 用户在看头文件时只需要看到他能使用的接口即可, 看到一堆其他的东西会干扰阅读, 其次暴露太多实现细节也不好

可以用 pimpl 来实现 implementation 的隐藏, 原理比较简单, 只贴代码了:

// server.h
class Server {
    public:
        Server();
        void start();
    private:
        class ServerImpl;
        std::unique_ptr<ServerImpl> impl;
};

// server.cpp
Server::Server(): impl(std::make_unique<ServerImpl>()) {}
void Server::start() {
    while (true) {
        impl->receive_packets();
    }
}

class Server::ServerImpl {
    public:
        void receive_packets() {
            // ...
        }
    private:
        void handle_packet();
        void handle_packet_data();
        void handle_packet_ping();
        void handle_packet_pong();
    private:
        int socket;
        // more members ...
}

这样所有的实现细节都被隐藏到 source file 里了

lambda, std::function, etc

1. 问题

之前写代码遇到了一些传递回调函数的需求, 例如:

receive_packet_and_handle([](const char* buf, int len){
    // do sth. with buf
});

这样 receive_packet_and_handle 有两种写法:

  1. template<typename F>
    void receive_packet_and_handle(F&& handler) {
        // ...
    }
    
  2. using handler_t = std::function<void(const char*, int)>;
    void receive_packet_and_handle(const handler_t& handler) {
        // ...
    }
    

想知道两种传参有没有性能上的不同

2. 结论

先说结论, template 风格性能通常比 std::function 风格好, 前者能 inline lambda, 后者通常不能 inline, 可能还需要构造 std::function 对象

参考 can-be-stdfunction-inlined-or-should-i-use-different-approach 的回答

std::function is not a zero-runtime-cost abstraction. It is a type-erased wrapper that has a virtual-call like cost when invoking operator() and could also potentially heap-allocate (which could mean a cache-miss per call).

The compiler will most likely not be able to inline it.

If you want to store your function objects in such a way that does not introduce additional overhead and that allows the compiler to inline it, you should use a template parameters. This is not always possible, but might fit your use case.

不过之前想用 std::function 的另一个原因是它能约束传入的 lambda 的参数及返回值, 用模板的话当传入的函数签名不符合预期时会有比较晦涩的报错, 而且函数使用者也很难直接从函数声明看出来到底要传入什么 lambda, 返回值如何, 可读性很差

对于这个问题, 查到的解决办法是用 std::invocable, 参考 how-can-i-restrict-lambda-signatures-in-c17-template-arguments

3. 汇编

在 Compiler Explorer 上用 x86-64 gcc 12.2 测试如下代码

template<typename F>
void with_tempalte(F&& f) {
    f(10);
}

void with_function(std::function<void(int x)>&& f) {
    f(10);
}

static volatile int a;
void test() {
    // #1
    with_tempalte([](int x){
        a = x;
    });

    // #2
    with_function([](int x){
        a = x;
    });
}

O0

1. 调用 with_template

lea     rax, [rbp-65]
mov     rdi, rax
call    void with_tempalte<test()::{lambda(int)#1}>(test()::{lambda(int)#1}&&)

2. 调用 with_function

lea     rdx, [rbp-17]
lea     rax, [rbp-64]
mov     rsi, rdx
mov     rdi, rax
call    std::function<void (int)>::function<test()::{lambda(int)#2}, void>(test()::{lambda(int)#2}&&)
lea     rax, [rbp-64]
mov     rdi, rax
call    with_function(std::function<void (int)>&&)
lea     rax, [rbp-64]
mov     rdi, rax
call    std::function<void (int)>::~function() [complete object destructor]
jmp     .L19
mov     rbx, rax
lea     rax, [rbp-64]
mov     rdi, rax
call    std::function<void (int)>::~function() [complete object destructor]
mov     rax, rbx
mov     rdi, rax
call    _Unwind_Resume

std::function 时会构造 std::function 对象, 后者显然更低效

O1

1.

lambda 被直接内联了, 汇编只有

mov     DWORD PTR a[rip], 10

2.

mov     rdi, rsp
call    with_function(std::function<void (int)>&&)

with_function(std::function<void (int)>&&):
        sub     rsp, 24
        mov     DWORD PTR [rsp+12], 10
        cmp     QWORD PTR [rdi+16], 0
        je      .L9
        lea     rsi, [rsp+12]
        call    [QWORD PTR [rdi+24]]
        add     rsp, 24
        ret
.L9:
        call    std::__throw_bad_function_call()

仍然存在对 std::function 对象的调用, 没有内联

O3

        mov     DWORD PTR a[rip], eax
        ret
with_function(std::function<void (int)>&&):
        sub     rsp, 24
        cmp     QWORD PTR [rdi+16], 0
        mov     DWORD PTR [rsp+12], 10
        je      .L10
        lea     rsi, [rsp+12]
        call    [QWORD PTR [rdi+24]]
        add     rsp, 24
        ret
.L10:
        call    std::__throw_bad_function_call()
test():
        mov     DWORD PTR a[rip], 10 # with_template
        mov     DWORD PTR a[rip], 10 # with_function
        ret

O3 情况下二者都被内联了, 但此时 with_function 仍然出现在了汇编结果里

malloc c++ class 而不调用构造函数引发的 segfault

背景

原本有一个纯 C 的库,其某结构体内保存了一个函数指针作为 callback

struct Foo {
    int (*output)(...);
};

为了在 C++ 中方便地使用 lambda 等,我自作主张地将其改成了

struct Foo {
    std::function<int(...)> output;
};

起初工作得很好,std::function 在替代函数指针方面非常方便。直到将代码放到另一套环境中时发现会随机 segfault,gdb 调试定位到了该回调相关的部分。一定是遇到了 Undefined Behavior 了

排查

出现问题的代码片段位置不尽相同,但是总是出现在 给该结构体的 output 成员赋值 上,如

// when creating a empty Foo instance, give it's member a default value
foo->output = std::function<int(...)>();

segfault 的 backtrace 最终总是

#0  0x0000000000404ce4 in std::_Function_base::~_Function_base (this=0x7fffffffc3c0, __in_chrg=<optimized out>)
#1  0x000000000040ca7e in std::function<int (...)>::~function()
#2  0x00000000004180df in std::function<int(...)>::operator=<::<lambda(...)> >::<lambda(...)> >(struct {...} &&) 

即调用 std::functionoperator= 赋值时,调用了某个 std::function 的析构函数,然后析构函数触发了 segfault。根据赋值的位置,应当是将新的 output 赋值到成员变量时,原本成员变量的 output 需要析构

然后想到因为是纯 C 的库,申请 Foo 的位置均使用 mallocmalloc 默认不会构造结构体及其 member 的构造函数,因此得到的 foo->output 的位置上也是未初始化的、随意一段内存,却被当成了一个合法的 std::function。在这段随意的内存上试图调用 std::function 的函数则成为了 UB,导致了 segfault

因此解决方式也很简单,在 malloc 之后手动 placement new 一下结构体(或者是单独初始化 output)即可

Foo* foo = (Foo*)malloc(sizeof(Foo));
new (&(foo->output)) std::function<int(...)>();

priority queue with updatable priority

事情是这样的,我正在维护一组会话,每个会话每隔一段时间都需要被轮询,每个会话轮询的间隔都是不等且变化的 —— 每次轮询之后,它会告诉我下次什么时候来重新轮询

当然,在那个它告诉我的 “下次轮询时间” 之前轮询它并不会有什么作用(也没有副作用),在那个时间之后轮询则会产生一点效率损失,比如会话上的包被更晚处理了,之类的

因此一开始我的实现非常粗暴:持续地以一个非常小的时间间隔 tick,每个 tick 都遍历所有的会话并轮询它们。但假设一个会话 100ms 后才需要再次轮询,我却每 5ms 都轮询一遍所有会话,显然做了许多无用功

于是我希望用优先队列来安排每个会话的轮询时间,当然,最先想到的就是 std::priority_queue

但还有一些需求无法被满足,因为会话的轮询还有一个变量:会话的下次轮询时间可能会改变

  • 假如会话 A 原本计于 100ms 后被再次轮询,但其上产生了一个新的消息(或是什么事件),则会话 A 可能需要在 10ms 后就被轮询

放到优先队列的语境里,即一个队列内元素的优先级可能会被改变,但 std::priority_queue 并不提供改变优先级的接口,事实上它也不能做到,因为它基于堆实现,想修改堆内任意一个元素的值而不破坏堆的性质,需要以 O(N) 的代价重建堆来实现,但我们期望一个 O(log n) 的更新手段

类似的需求也在 Dijkstra 算法中被用到,其解决办法在 Easiest way of using min priority queue with key update in C++ 中被提到:在 Dijkstra 算法对优先队列的需求中,可以通过 "lazy deletion" 来实现类似的功能,即不考虑更新元素的优先级,而是直接将更小优先级的元素插入队列中,新插入的元素总是会比旧元素更早被 pop,只需要加上去重逻辑,就变相 “更新” 了更新旧元素的优先级

但是在本项目中,我觉得这个方法并不适用,因为在 Dijkstra 中图的大小是 bounded 的,而会话上消息的产生是 unbounded 的,如果每次有新消息产生并要更新会话优先级时,都插入一个新元素,那 priority queue 可能会爆掉,或者产生效率问题(比如 pop 的时候需要去除大量重复元素)

因此,我希望有这样一个优先队列,它能够以某个优先级安排队列内所有元素的顺序,又可以快速地更新某个元素的优先级

实现

一个简单的实现如下

template <typename Priority, typename Val>
class UniquePriorityQueue
{
public:
    void push_or_update(const Priority &p, const Val &v)
    {
        auto vp_it = _vp_map.find(v); // O(log n)
        if (vp_it != _vp_map.end())
        { // update
            auto pv_it = _pv_map.find(vp_it->second); // O(log n)
            assert(pv_it != _pv_map.end());
            _pv_map.erase(pv_it); // O(1)

            vp_it->second = p;
            _pv_map.insert({vp_it->second, vp_it->first}); // O(log n)
        }
        else
        {
            auto [vp_it, success] = _vp_map.emplace(v, p); // O(log n)
            assert(success);
            _pv_map.insert({vp_it->second, vp_it->first}); // O(log n)
        }
    }

    void pop()
    {
        auto pv_it = _pv_map.begin();
        assert(pv_it != _pv_map.end());
        auto vp_it = _vp_map.find(pv_it->second);
        assert(vp_it != _vp_map.end());
        _pv_map.erase(pv_it);
        _vp_map.erase(vp_it);
    }

    const std::pair<const Priority&, const Val&> top() const {
        auto pv_it = _pv_map.begin();
        assert(pv_it != _pv_map.end());
        return {pv_it->first, pv_it->second};
    }

    bool empty() const {
        assert(_pv_map.size() == _vp_map.size());
        return _vp_map.empty();
    }
private:
    std::map<Priority, const Val &> _pv_map;
    std::unordered_map<Val, Priority> _vp_map;
};

之所以叫 UniquePriorityQueue,是因为队列内的 Val 是不重复的,如果一个 Val 已经存在,那么 push_or_update 的语义就是更新其优先级

各接口的复杂度为:

  • O(1): top(), empty()
  • O(log n): push_or_update(), pop()

以下是一个完整可编译的程序(C++20), 模拟了刚才所说的安排会话的例子

#include <map>
#include <unordered_map>
#include <vector>
#include <memory>
#include <cassert>
#include <random>

template <typename Priority, typename Val>
class UniquePriorityQueue
{
public:
    void push_or_update(const Priority &p, const Val &v)
    {
        auto vp_it = _vp_map.find(v);
        if (vp_it != _vp_map.end())
        { // update
            auto pv_it = _pv_map.find(vp_it->second);
            assert(pv_it != _pv_map.end());
            _pv_map.erase(pv_it);

            vp_it->second = p;
            _pv_map.insert({vp_it->second, vp_it->first});
        }
        else
        {
            auto [vp_it, success] = _vp_map.emplace(v, p);
            assert(success);
            _pv_map.insert({vp_it->second, vp_it->first});
        }
    }

    void pop()
    {
        auto pv_it = _pv_map.begin();
        assert(pv_it != _pv_map.end());
        auto vp_it = _vp_map.find(pv_it->second);
        assert(vp_it != _vp_map.end());
        _pv_map.erase(pv_it);
        _vp_map.erase(vp_it);
    }

    const std::pair<const Priority&, const Val&> top() const {
        auto pv_it = _pv_map.begin();
        assert(pv_it != _pv_map.end());
        return {pv_it->first, pv_it->second};
    }

    bool empty() const {
        assert(_pv_map.size() == _vp_map.size());
        return _vp_map.empty();
    }
private:
    std::map<Priority, const Val &> _pv_map;
    std::unordered_map<Val, Priority> _vp_map;
};

struct Job
{
    int id;
    int i = 0;
    std::vector<int> schedule;

    Job(int id) : id(id) {}

    int next()
    {
        if (i < schedule.size())
        {
            return schedule[i++];
        }
        else
        {
            return INT32_MAX;
        }
    }
};

constexpr int JOBS = 1000;
constexpr int ITERATIONS = 1000000;

int main()
{
    std::srand(time(nullptr));

    UniquePriorityQueue<int, std::shared_ptr<Job>> q;

    std::vector<std::shared_ptr<Job>> jobs;
    std::vector<Job *> schedule;

    // generate N jobs
    for (int i = 0; i < JOBS; i++)
    {
        jobs.push_back(std::make_shared<Job>(i));
    }

    // tick ITERATIONS times, each tick one random job should be done, use `schedule` to record the order
    for (int priority = 0; priority < ITERATIONS; priority++)
    {
        auto job = jobs[std::rand() % JOBS];
        schedule.push_back(job.get());

        job->schedule.push_back(priority);
    }

    // init priority queue
    for (auto& job: jobs)
    {
        auto next = job->next();
        q.push_or_update(next, job);
    }

    // tick, each tick get the job with the lowest priority, and update its priority
    for (int i = 0; i < ITERATIONS; i++)
    {
        printf("\rticking %d/%d", i, ITERATIONS);
        const auto &[p, job] = q.top();

        // check if the order is correct
        assert(p == i);
        assert(job.get() == schedule[i]);

        // schedule next
        auto next = job->next();
        assert(next > p);

        q.push_or_update(next, job);
    }
    assert(q.top().first == INT32_MAX);
    return 0;
}

部署 mdbook 到 github pages

本网站由 mdbook 生成,通过 github actions 持续集成到 github pages,以下为部署方式

1. 为项目创建仓库

BlackCloud37/mdbook-blog,其 master 分支为 mdbook 项目

2. 添加 Github Action

参考 GitHub Pages Deploy,创建 .github/workflows/deploy.yml,内容为

name: Deploy
on:
  push:
    branches:
      - master

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v2
      with:
        fetch-depth: 0
    - name: Install mdbook
      run: |
        mkdir mdbook
        curl -sSL https://github.com/rust-lang/mdBook/releases/download/v0.4.14/mdbook-v0.4.14-x86_64-unknown-linux-gnu.tar.gz | tar -xz --directory=./mdbook
        echo `pwd`/mdbook >> $GITHUB_PATH
    - name: Deploy GitHub Pages
      run: |
        # This assumes your book is in the root of your repository.
        # Just add a `cd` here if you need to change to another directory.
        mdbook build
        git worktree add gh-pages
        git config user.name "Deploy from CI"
        git config user.email ""
        cd gh-pages
        # Delete the ref to avoid keeping history.
        git update-ref -d refs/heads/gh-pages
        rm -rf *
        mv ../book/* .
        git add .
        git commit -m "Deploy $GITHUB_SHA to gh-pages"
        git push --force --set-upstream origin gh-pages

之后所有到 master 的 push 都会 build 当前的 mdbook,并将产物 push 到本仓库的 gh-pages 分支

3. 访问网站

gh-pages 分支下的网页会被部署到 <Username>.github.io/<Reponame> 域名下,如本项目即为 blackcloud37.github.io/mdbook-blog,直接访问即可

4. Trouble shooting

第一次推送触发 github action 后,gh-pages 分支存在并且已经包含产物,但是访问 blackcloud37.github.io/mdbook-blog 提示 404,参考 https://stackoverflow.com/questions/11577147/how-to-fix-http-404-on-github-pages,推送一个空 commit 到 gh-pages 分支即可:

# 在 gh-pages 分支
git commit --allow-empty -m "Trigger rebuild"
git push

另外,仓库可见性必须为 Public

实现校外 IPv4 访问北邮人

为之后毕业离校做打算,用 SSR + 海外 IPv6 服务器做代理访问 byr

搭建服务器

  1. Vultr 买个最便宜的机器,为了稳定系统选了 CentOS 7,记得 enable ipv6

  2. 参考 SSR一键安装 安装 ssr 服务端

    wget --no-check-certificate -O shadowsocks-all.sh https://raw.githubusercontent.com/teddysun/shadowsocks_install/master/shadowsocks-all.sh
    chmod +x shadowsocks-all.sh
    ./shadowsocks-all.sh 2>&1 | tee shadowsocks-all.log
    

    配置选择

    Your Protocol         :  auth_aes128_sha1
    Your obfs             :  tls1.2_ticket_auth
    Your Encryption Method:  aes-256-cfb
    

    相关命令

    启动SSR:
    /etc/init.d/shadowsocks-r start
    退出SSR:
    /etc/init.d/shadowsocks-r stop
    重启SSR:
    /etc/init.d/shadowsocks-r restart
    SSR状态:
    /etc/init.d/shadowsocks-r status
    卸载SSR:
    ./shadowsocks-all.sh uninstall
    

    配置文件: /etc/shadowsocks-r/config.json,可以检查一下里面有没有 server_ipv6,以及需要将 dns_ipv6 改为 true

客户端

  1. 安装 ssr 客户端
    • 对于 osx,可以用 brew install shadowsocksx-ng-r --cask,配置连接
  2. 在 ssr 客户端里添加 byr 的规则以对相关域名使用代理

下载

参考 如何在普通网络环境下上北邮人

以 qBittorrent 为例,需要配置

  1. Connection - Proxy 填入 ssr 的 local 代理地址/端口
  2. 关闭 Advanced 里的验证 tracker 证书

Reference

数据安全

给科技服务队写的,介绍如何备份数据

0. 关于最常见的不良习惯

本文的正文从第 1. 节开始,但由于大部分数据丢失与一些不良使用习惯相关,我们在这个单独的置顶的篇幅中罗列这些习惯的 Checklist,以便引起足够的重视。这些行为将极大增加你丢失重要数据的概率,请详细阅读:

  1. 移动、震动、磕碰运行中的移动机械硬盘。这是我们发现的最常见的移动机械硬盘的损坏原因之一。运行中的机械硬盘必须被稳定放置,避免任何形式的机械震动,原因参考 3.4 节

    如果机械硬盘在使用中遭到磕碰,并发出咔哒或者沙沙的摩擦声,且硬盘不可读,请立刻断电并寻求帮助。

  2. 只在一个设备上保存你的重要文件,例如将所有重要资料单独保存(与备份相对应)在电脑的一块硬盘(Windows 的多个盘符可能属于同一块硬盘)、移动硬盘、U盘、SD卡中

  3. 在任何可移动存储设备上长期保存重要资料,尤其是 U盘、移动机械硬盘。基本上你可以视它们为随时都可能损坏的设备,细节参考 3.4 节

  4. 强行断开任何使用中的存储设备的电源,例如热插拔移动硬盘、未关机状态下断开台式机电源等,这有概率导致文件系统损坏甚至是存储设备物理损坏(尤其是机械硬盘,参考 3.4 节)

  5. 随意下载安装软件,随意在打印店等公开场所使用U盘并将其插回自己的电脑上

1. 序言

数据安全与 “数据丢失” 对应,指 “保护你的关键数据不丢失” 及 “维护你工作系统数据完整性” 的过程。在我们的经验中,(数据安全)通常对你的学习、工作生活至关重要,但通常也被大部分没有相关经验的人忽视,经常导致一些严重事故(例子参考第 2. 节)。

本文将介绍科技服务队遇到过的常见数据丢失例子,旨在引起不关注数据安全的人的重视(第 2. 节),并介绍数据丢失的成因及为何其通常比许多人想象的严重,并简单介绍急救原则(第 3. 节),最后介绍维护数据安全的基本原则和具体工具(第 4., 5. 节)。

如果你发现可能遇到了数据丢失相关的事件,可以参阅第 3. 节来获取可供参考的急救手段。

如果你只是希望学习日常维护数据安全的方法和习惯,请直接阅读第 4., 5. 节。

2. 常见数据丢失例子

在阅读之前,我们通常可以从一个角度出发:目前对你而言最重要的数据是什么,如果它此刻完全消失不可恢复,你的生活会因此受到多大的冲击。

例如,你丢失了:

  • DDL 是今晚的作业,写了一天但还没提交
  • 陆续经营几年的游戏存档
  • 过去几年的照片、聊天记录等
  • 大学期间的各类申请材料,也许与你正在进行的申请相关
  • 工作数月的大作业、课程设计或毕业设计
  • 几年的数 TB 的实验数据,或者撰写中的论文稿

通常如果上述事件真实发生,你的学习、工作将会受到很大影响。而且在我们的经验中,上述每一项都有人经历过(甚至多次),并且都是以一种不可恢复的形式。它们可能会由以下具体事件导致(同样是我们经历过的):

  1. 误操作导致撤销、删除重要文件或格式化含有重要数据的设备
  2. 由于数据被加密(Bitlocker 等)并丢失了密钥导致设备数据无法读取
  3. 由于下载不明渠道的软件或在不可信的地方使用USB设备导致中毒
  4. 存储设备物理损坏
  5. 设备丢失、自然灾害等不可抗原因

在下一节中将简单介绍上述事件的成因及后果。

3. 数据丢失的成因及后果

3.1 误操作

成因

通常是疏忽,或是由于对使用的系统及自己的操作不理解。例如不注意自己删除了哪些文件(删除时选中了过多的文件);不理解 Ctrl-Z(Win)/Cmd-Z(Mac) 对应的撤销操作会导致自己创建文件的行为被撤销导致文件消失;不理解格式化、重置系统(清除用户数据)的含义或是不经确认就进行这类操作;不熟悉网盘或是 Git 等涉及文件管理的软件的操作导致误操作等。

后果

通常与删除操作发生到现在经过的时间、及期间硬盘上写入事件发生的次数相关(例如写入文件等)。同时,如果数据在机械硬盘(HDD)上,恢复概率较高,在固态硬盘(SSD)上时难以恢复。

⚠️注意:在现在(2023年)的常见 PC 中,由于绝大部分 PC 都使用固态硬盘(SSD)安装系统,且通常会开启 TRIM,故上述操作不可恢复的概率非常高。这是由于 HDD 与 SSD 的存储机制与空间管理机制不同导致的,例如前者不会经常擦除已经删除的文件的数据,只会在有必要时覆盖,后者会通过回收已删除文件的数据块的机制来保证写入速度,但这导致被删除的文件的数据会很快消失。

建议

发现之后立刻停止创建文件等可能会对数据所在盘做写入操作的行为,并询问专业人士下一步操作,在不清楚你在做什么的情况下不要自行通过安装软件等方式试图恢复数据。

3.2 数据加密

成因

通常是 Windows 系统下的 Bitlocker 导致的,简单来说它会通过软硬件的加密手段将你的整块硬盘上的数据加密,在你的系统遭到破坏时(许多原因会导致这个机制被触发),必须使用一个密钥才能恢复数据。这个密钥可以通过 在 Windows 中查找 BitLocker 恢复密钥 找到,也可以在你系统可以正常使用时导出。

后果

如果你没有密钥并且 Bitlocker 被激活,几乎没有任何手段可以恢复你的数据。

建议

务必妥善备份并保管你的密钥。或者可以提前关闭 Bitlocker,但这会有数据泄露风险,因此这不构成我们的建议。

唯一的恢复手段是找到密钥的备份,或者通过微软账户找到密钥。

3.3 病毒及恶意软件

成因

缺乏安全意识的用户可能无法分辨网上软件的安全性,当用户在使用国内搜索引擎时尤甚,会误下载到病毒软件。一些流氓软件(也可能以杀毒软件或电脑管家的形式出现)也在此范畴。Windows 为此类软件的重灾区。

校内一些公共场合的设备由于使用者众多可能是包含病毒的,在这些设备上使用了你的 USB 设备可能会导致 USB 设备中毒,可能会导致数据被破坏,或者将病毒带到你自己的机器上导致更严重的后果。

同时,如果用户安装了一些会对外暴露端口的软件/服务,未设置防火墙并在无 NAT 的网络环境下(校园网就是一种),会导致这些端口被攻击,许多服务都不会设置用户鉴权,并可能有很多已知漏洞,因此任何一个攻击者都可能通过非常简单且自动化的方式攻击这些服务,并对运行服务的机器做许多事。其中包括加密你的数据并借此勒索。

后果

通常视病毒及勒索软件的种类而定,例如一些此类病毒/软件可能已经有已知的恢复被破坏数据的方法。但是如果没有对应的方法,数据将难以恢复。

建议

使用正版软件,从可信的渠道下载软件,包括官网、清华的 its.tsinghua.edu.cn、操作系统官方的应用商店 (Windows 的应用商店、Mac 的 AppStore) 等,不包括第三方的软件管家、搜索引擎上找到的软件下载站等。如果你不能确认你是否正从正确的地方下载软件,请咨询了解的同学。

避免安装各类第三方杀毒软件、电脑管家。但开启系统自带的防护机制,例如 Windows Defender。

不要在 USB、移动硬盘等会在不受信任的其他电脑上(例如打印店)使用的设备上存放重要数据,如果有,请提前在其他地方备份。

如果遇到了数据被破坏、加密勒索的情况,请咨询专业人士如何恢复(通常需要对症下药)。

3.4 存储设备损坏

成因

常见的存储设备有机械硬盘(HDD)、固态硬盘(SSD)和闪存(U盘)。

其中机械硬盘由于依赖磁头在高速旋转的盘片上读写数据,因此非常容易因为震动、磕碰、意外掉电等情况导致磁头摩擦盘片,进而导致损坏,导致数据丢失。供电不足(常见于 USB 移动硬盘 + 拓展坞的组合,或者是劣质硬盘盒)或者机械结构本身的磨损也会导致类似事件发生。

固态硬盘的颗粒擦写寿命有限,且目前消费级产品的颗粒寿命较短,在累积写入过多数据之后一些块会不可逆地损坏,最终导致数据丢失。这个寿命指标通常会在固态出售时以 TBW 的形式告知用户,如 300TBW 表明其设计寿命是总共写入(Write) 300TB。容量越大的固态硬盘该指标通常越大,即寿命越长。

闪存的存储原理与固态硬盘类似,但注意大部分 U盘 的产品定位不是长期稳定存储数据,而是短期移动数据,因此其寿命非常不可靠。不要将任何重要数据单独存放在 U盘 中。

后果

机械硬盘损坏后,只要盘片保存良好并且上面没有过多磨损,可以通过专业数据恢复机构开盘读取磁信息来恢复数据,但价格通常较高。

固态硬盘与 U盘 损坏后,数据通常无法恢复,且其寿命不可靠,因此不要将任何重要数据单独存放在它们上。

建议

经常查看硬盘的 S.M.A.R.T. 信息,可以将它理解为硬盘的健康度。正常使用的硬盘通常不会突然损坏,损坏是一个渐进的过程。例如硬盘设计通常会给坏块预留缓冲,即一些备用块可以被用于替换损坏的块,当这些备用块被使用时,SMART 会给出相应的警告数值,当备用块用完时才会出现更严重的事。在此之前查看 SMART 即可了解相关问题,并及时处理。也可以通过类似 Disk Genius 的软件扫描 HDD 的坏道,确认没有坏道时再在其上存储数据。

对于任何存储设备,避免突然断电(例如拔掉台式机电源,或是热插拔运行中的移动存储设备)。不要使用劣质的硬盘盒或转接头/扩展坞(尤其针对移动 HDD)。

对于 HDD,避免在其运行时震动甚至磕碰它。

对于 SSD 和 U盘,长期不通电时,其上的数据可能会逐渐由于电荷流失而丢失,因此不要试图用这些设备长期存储冷数据。如果有类似的需求,请使用 HDD、光盘甚至磁带。

3.5 设备丢失等

对于可移动设备这比较常见(例如手机或者 U盘),手机中可能会有照片、聊天记录等重要数据,因此建议参考后面的章节经常备份数据。

4. 维护数据安全的基本原则

除了第 3. 节中提到的各种建议外,本章将介绍通用的维护数据安全的原则:备份

⚠️ 没有任何手段能代替备份。第 3. 节中提到的所有建议都不能替代备份,且考虑到其中提到的数据丢失原因均比较常见(没有侥幸),如果你发现你有对你而言重要的数据,且它们没有按照本章中描述的原则被正确备份,强烈建议你参考本章进行备份。

备份原则:3-2-1

这是一个备份的黄金准则,少于这个标准的备份在意外情况下可能无法恢复数据,因此失去备份的意义

  • 3: 重要文件需要被完整存储 3 份,一份原件,两份拷贝。
  • 2: 三份文件需要被保存在至少 2 种不同的介质上,如 HDD、SSD、磁带、光盘等(不建议使用 U盘),同时电脑自带的硬盘和外置硬盘也可以视为不同介质,HDD/SSD 在家用场景下比较容易获取。
  • 1: 三份文件中至少有 1 份保存在异地。

3 份备份保证数据同时被毁的概率足够低,因为这个概率随着备份变多指数降低。

2 份不同介质是由于相同介质通常会由于相同的原因损坏,因此它们同时损坏的概率较高,例如一次电脑意外断电 可能 会同时导致你电脑里的所有 HDD 都挂掉,但 SSD 可能相对更不容易在这种情况下损坏;或者电脑中毒可能会导致电脑上的所有数据损坏,但外置硬盘不会受影响。不同介质保存数据的物理形式也不同,因此在相同的环境下同时损坏的概率较低。

1 份异地通常是考虑到自然灾害或者盗窃等的影响。异地不一定要很远,例如宿舍和实验室/公司/家里也可以算异地。这也可以通过各类可靠的云盘实现(*度云等可能会篡改你数据的云不在此列),且云盘比自行维护异地的物理存储更方便,因此更为推荐。

最简单且常见的例子是,在你的工作机上保存一份数据原件(通常在 SSD 中),同时定期(最好通过自动化的方式)将其拷贝到一份外置的机械硬盘中,这样就做到了 2 份文件 + 2 种介质,最后再在一个云盘中存储一份备份,即完成了 3-2-1 原则。

5. 常见备份方法

注1:本节中的 备份 和 同步 可能会被统称为备份。但狭义上,备份指将数据的某个状态保存到备份设备中,并可能包含压缩和去重等操作,同步则是连续地时刻维持两个设备上的数据相同。由于同步无法保证误操作删除数据时远端数据不被删除,因此其不能替代备份。

注2:可能有用户会使用 RAID,但这里认为任何形式的 RAID 都不是备份,也不能替代备份的功能。建议将 RAID 后的组作为单块介质看待。

本节主要介绍实现 3-2-1 备份的可用手段及工具。参考第 4. 节中的例子,我们假设数据原件在一台装有 SSD 的电脑中,并希望备份到一个本地外置硬盘 + 一个云

数据备份的间隔取决于你要备份数据的重要程度,例如你可以忍受几天的数据丢失。另外,任何里程碑式的数据更新(例如拍了一批新照片、有阶段性的工作成果)之后也建议完整备份数据。

5.1 本地备份

当你只需要备份少量重要数据文件(如文档等)时,你可以直接像平时使用移动硬盘一样备份这些数据:格式化外置硬盘(注意选择日志式文件系统,如 Windows 常见的 NTFS,或是 Ext4 等),在里面创建文件夹并存放你的文件。然后记得妥善保存这块外置硬盘。

在 Windows/Mac 下,你也可以借助一些数据同步软件来更便捷地完成这个过程,例如你总是需要将系统中的某个文件夹同步到外置硬盘中的另一个文件夹,这个机械化的操作可以通过一些第三方的数据备份工具/软件进行,在设置之后可以一键执行备份操作,也可以定时触发备份等。同时当你备份的文件夹包含较多备份过的重复文件时,这类软件提供的 diff 算法可能能更高效地完成备份(而不是像系统自带的资源管理器,需要覆盖每个重名文件)。

如果你在使用 MacOS,可以通过其自带的 Time Machine 完整地备份系统的多个快照,恢复到任意一个其中包含的时间点(包括系统状态和系统里的数据)。因此强烈建议 Mac 用户使用至少一块和系统硬盘一样大的外置硬盘来定期做 Time Machine。

Linux 用户可以使用 Rsync 进行备份,通过 cronjob/system service 进行定时操作,对于特定的文件系统(例如 Btrfs/Zfs),也可以使用快照的方式实现类似 Time Machine 的备份效果,但注意在同一块盘上创建的快照不能视为备份。请至少将其保存到另一个介质中。

当你需要备份一些不经常访问的冷数据时(例如归档学习资料),将它们压缩成压缩包后再进行备份操作效率会更高。一些专业的备份软件也会提供压缩和去重的操作,常见于一些商用 NAS 系统的备份工具,但这可能需要更复杂的条件和设备。

5.2 多端备份

你可以选择将数据备份到一块单独的介质中并保存到异地来实现这一点。不过使用云盘通常是一种更便捷的方式。本小节将介绍常见的云盘。

  • 国内云盘存放的数据可能会未经你的同意被泄露、删除、替换。在使用之前请认真考虑这一点。如果一定要使用,建议将数据加密并压缩再上传,且不要将鸡蛋放在一个篮子里。

  • 本节提到的云盘与科技服务队利益无关。

清华云盘

⚠️ 清华云盘会在毕业时被回收。因此一定注意在毕业前将其上的数据备份到其他地方。包括个人资料库及个人创建的群组资料库。

清华大学为学生提供了一个可用的云盘,可以通过校内账号访问,其 使用指南 介绍了细节。它基于 SeaFile 搭建,Web 界面 可以像普通网盘一样使用。同时 Seafile 也有客户端,可以在 Windows/Mac/Ubuntu/Debian/Fedora 及 Android/iOS 中使用。客户端支持同步盘和挂载盘两种模式。具体使用细节均在上述使用指南中被详细描述。

其中,同步盘 “可以实现将云盘资料库和本地文件夹(目录)关联和自动同步的功能。用户关联云盘资料库和本地文件夹后,客户端将自动同步本地和云端内容,本地和云端的文件和目录的新建、修改、删除、重命名等变化都会保持一致“,即将本地某个目录完全同步到云端,二者的文件会维持一致的状态。此时文件在本地云端各有一份。

同步盘在正确同步后,假设本地硬盘突然损坏,在云端应当能找到最后一次同步时的数据,起到备份的效果。但是由于同步会实时进行,可能会由于网络原因出现意料外的 Bug。同时如果在本地误操作删除/修改了同步盘中的文件,该操作可能会被即时同步到远端,因此这种同步的方式严格来讲不能算备份(即使删除操作可能可以通过 Seafile 的回收站找回)。

挂载盘则相当于将云盘资料库单独作为本地的一块磁盘使用,此时电脑上会多出一块虚拟的 “硬盘”,其被映射到云盘的资料库。与同步盘的区别在于,此时文件只在云端有一份,本地硬盘上并没有相关数据。因此可以将其作为网盘 Web 界面的本地版使用,可以较方便地实现将本地文件上传到云盘的操作。

上述方式可以根据需求选择,例如在 3-2-1 原则下:

  • 如果使用同步盘,则只需要维护本地文件夹和一块外置硬盘,本地文件夹会被同步到云端资料库,达到 3-2-1 的要求(由于这种同步方式严格来讲不能算备份,建议使用挂载盘模式)
  • 如果使用挂载盘,则本地源文件夹为一份数据,本地挂载盘即云端资料库,为一份远端数据,备份时需要同时将本地源文件夹中的数据备份到本地挂载盘和外置硬盘(二者操作几乎相同,因为这种情况下挂载盘和外置硬盘都可以视为单独的备份盘),达到 3-2-1 的要求。通过 Web 界面管理文件本质与挂载盘相同。

坚果云

国内网盘,提供不限量的免费空间,但是限制了每月上传 1GB、下载 3GB。优势是相对没有那么流氓,例如免费版也不会限速。其也提供了多平台的客户端,提供了普通网盘的使用体验。

同时其支持 WebDAV 协议(坚果云第三方应用授权WebDAV开启方法),因此可以在不适合安装客户端但支持 WebDAV 的系统下挂载(Windows/Mac/Linux 均可)。

阿里云盘/腾讯微云/百度云盘等

均为国内使用量比较多的云盘,此外还有联通/移动/电信等运营商的云盘和蓝奏云盘等。可以根据其定价策略和使用体验来选择。

OneDrive/iCloud

二者分别为 Windows 和 Mac/iOS 自带的云盘,分别需要微软账户和 Apple 账户使用。作为系统内置的云盘,其主要目标是实现多端同步(例如 Mac/iOS 设备间的照片、文档同步,多台 Windows 设备间的资料同步),它们均提供了一定量的免费空间(但比较小),且基本模式与清华云盘的同步盘模式相同,能够将系统的指定文件夹(桌面、文档、照片等)同步到远端,并让多台设备共享这些文件夹。由于同步严格来说不能算备份,因此不建议将它们视为备份手段。不再展开介绍。

同时,iCloud 还可能会由于 Apple 账号登出等原因导致本地数据被抹掉,又由于网络原因难以从云盘中重新下载,因此对于重要数据请遵循 3-2-1 原则并寻找其他备份手段。

Google Drive/Dropbox/Mega

均为国外网盘,适合有条件访问的人使用。

对象存储服务

这类服务通常是面向开发者的,使用起来可能不会有面向个人的云盘方便,因此只是提供一个思路。

且这类服务通常没有免费版,虽然某些情况下价格可以做到很低,例如 AliyunOSS 冷归档 50G 的数据只需要 9元/年。

一些云服务提供商会提供对象存储服务(例如阿里云 OSS、腾讯云 COS),其冷归档存储的计费较为便宜,以 Aliyun OSS 为例:冷归档数据需要至少存储 180 天,并且在取回时需要解冻(即等一段时间才能下载),上传不计费,取回时按数据解冻量收费,存储计费为 0.015元/GB/月,适合用于长期存储冷数据,即需要长期存储但几乎不会访问的(重要不常用的)数据,例如几年前的课程归档。

以 Aliyun OSS 为例,注册 Aliyun 账户后,可以参考 开始使用OSS 中的 使用 OSS 控制台

  1. 开通 OSS 服务
  2. 创建 Bucket(可以理解为一个资料库,所有设置都是以 Bucket 粒度区分的)
  3. 设置 Bucket (主要为存储类型,例如冷归档存储,并关闭传输加速等计费项)
  4. 在 Bucket 内创建文件夹并上传、下载

在 Bucket 创建后,也可以通过 开始使用OSS 中的 使用图形化管理工具ossbrowser 一节来获取一个客户端,通过客户端管理 Bucket 内的文件,达到类似使用网盘的效果,在备份的场景中是够用的。

在存储冷备份数据时,可以先在本地压缩并加密后再上传到云端,减少计费并保证数据安全,如果加密记得备份加密手段及密钥。

6. 不同成本的备份例子

本节列举多种不同成本(由低到高)的备份例子,供读者参考。选择方案时请综合考虑数据重要程度、数据量与数据访问/修改频繁程度。如果数据非常重要,则不需要拘泥于 3 份,可以选择更多份备份。

如果需要备份的数据性质明显不同,则对于不同数据可以选择不同的方式。例如古早的照片可能不需要经常修改、查看,但需要稳定保持,这类冷数据就建议放在移动机械硬盘中(并妥善存放),再留有至少一份放在云盘(或者是 OSS)中。而作业可能需要经常修改、查看,因此建议做 6.2 的双盘备份,在本地同步/备份会比较方便。对于代码类数据,则建议使用 Git 进行托管,但也需要在本地进行备份重要的 Commit/Tag 以便在 Git 误操作时挽救。

6.1 本机另一块硬盘同步 + 1个云盘

本方案最为简便,但容错差,数据重要时请从 6.2 开始

321 构成

  1. 源数据
  2. 电脑中的另一块硬盘(注意:Windows 下多个盘符不一定是不同硬盘)中的文件夹,保持数据与源数据同步
  3. 定期上传到任何云盘(清华云盘等),最好能自动上传

成本

  • 当电脑为固态 + 机械组合的笔记本、且云盘有一定白嫖容量时,这种方案几乎 0 成本
  • 如果电脑只有一块硬盘,则参考 6.2
  • 可以选择清华云或一些付费云盘以得到更好的体验

优点

  • 便宜
  • 本机双盘同步较为方便
  • 适合需要频繁修改、时效性短的数据,例如作业等

缺点

  • 容错低,本机出意外可能导致两块硬盘同时损坏(概率不容小觑)
  • 需要确保能够定期备份到云盘,过时的备份约等于没有备份
  • 上云数据要注意安全性

6.2 外置硬盘 + 1个云盘

本方案适合大部分人使用

321 构成

同 6.1,将其中的另一块硬盘换成外置硬盘

依据数据重要性,可以选择使用多块外置硬盘,并混合使用固态、机械硬盘,也可以在 6.1 的基础上增加一块外置硬盘

成本

  • 外置硬盘成本,例如现在(2023-04) 1T 的 NVME SSD 可能只需要 400 元人民币(或更低),加上不到百元的移动硬盘盒即可
  • 移动机械硬盘价格更低,但鉴于其容易由于震动等物理因素损坏,不建议将其作为“移动”用途,而是将数据备份到里面以后直接静置保存。且机械盘可能买到叠瓦盘,性能堪忧,需要做足功课
  • 除非要备份的是比较冷的数据,否则不建议移动机械硬盘方案

优点

  • 成本低
  • 容错率相对 6.1 更高

缺点

  • 本地备份相对 6.1 更麻烦(需要外接硬盘)
  • 移动硬盘要注意安全性(丢失、机械硬盘损坏等)
  • 移动固态硬盘要避免太久不上电

6.3 多云

鉴于许多云盘都有一定的白嫖容量,可以在 6.2 的基础上多选择几个适合自己的云盘供应商。这样的好处是不用担心某个云盘挂了。

6.4 私有云

本方案成本、技术要求较高,因此只是在此罗列而不详细介绍。对于普通用户的重要数据,只需要本地多外置硬盘备份 + 多云备份,基本就万无一失了

Arch 休眠到交换文件

参考 Arch WikiArch简明指南 配置系统休眠到 swap file(ext4),配置完毕后无法正常休眠,问题如下

1. KDE 开始菜单不展示休眠选项

尝试手动休眠 systemctl hibernate,提示 "Not enough swap space for hibernation"

根据 Arch BBS,通过 systemctl edit systemd-logind.service 并在其中添加

# 注意添加位置,必须在文件中注明的两段注释之间,否则不会生效
[Service]       
Environment=SYSTEMD_BYPASS_HIBERNATION_MEMORY_CHECK=1

后重启 logind 服务 systemctl restart systemd-logind 即可

2. 休眠后立刻回到登录页面

休眠后查看日志 journalctl -n 1000,在其中查找 hibernate 相关记录,发现报了

Failed to find location to hibernate to: Function not implemented

怀疑是 hibernate 目标交换文件配置有误,检查后发现在获取交换文件 resume_offset 时,用 sudo filefrag -v /swapfile 命令查看的偏移如下:

Filesystem type is: ef53
File size of /swapfile is 34359738368 (8388608 blocks of 4096 bytes)
 ext:     logical_offset:        physical_offset: length:   expected: flags:
   0:        0..    6143:    4114432..   4120575:   6144:            
   1:     6144..   38911:    3997696..   4030463:  32768:    4120576:
   2:    38912..   71679:    3506176..   3538943:  32768:    4030464:
   3:    71680..  104447:    8224768..   8257535:  32768:    3538944:
   ...

physical_offset列的第一个值应当是 4114432,而我配置成了 4120575,修改后重新生成 grub.cfg 即可

Wechat

20240125更新

备忘目前完整的微信安装流程

  1. 装 wine-for-wechat 和 wine-wechat-setup
  2. https://github.com/tom-snow/wechat-windows-versions 找老版本的微信安装包, 例如 Wechat v3.8.0.33

    wine-for-wechat 会默认使用 prefix / 'drive_c/Program Files/Tencent/WeChat' 作为 WeChat.exe 的目录, 安装微信时需要注意, 旧版本微信可能会默认安装到 Program Files x86 下

  3. 用 wine-wechat-setup 安装微信, 会一并创建 ~/.local/lib/wine-wechat 这个 WINEPREFIX

如果需要改 dpi 等, 可以 wechat -c

Archive

更新: 现在使用 com.qq.weixin.spark 包的微信,然后将其用的 wine 替换成 wine-for-wechat

使用打包好的 Deepin Wine Wechat Arch,仓库中已经给出了详细的安装方法、字体更换等

Sway

从 i3 迁移到 sway 后, Deepin Wine Wechat 的微信窗口黑屏, 暂时没找到解决办法, 改用 wine-for-wechat 后解决

安装流程

  1. 安装 wine-for-wechat, wine-wechat 包, 均在 archlinuxcn 源上
  2. 微信官网下载 .exe 安装包
  3. 命令行 wechat -i /path/to/wechat_setup.exe 安装微信
  4. 安装完成后就能使用, 如果字体有问题, 参考 Ubuntu20.04 Wine 6.0 微信中文显示方块/方框, 本质上为如下几步
  • winetricks 安装所有字体和所需 dll
  • 改注册表

不过仍然没解决 emoji 为方块的问题

Trouble shotting

窗口阴影

换到 i3wm 后,混成器使用了 picom,此时使用 Deepin Wine Wechat 会发现整个窗口被灰色遮罩,且有弧形的黑色阴影,关闭 picom 后上述情况消失,因此判断是 picom 的问题

查阅 arch wiki 上 picom 条目发现,picom 可以针对窗口禁用半透明、阴影等特性,其规则可以细化到匹配窗口名称

首先通过 xprop 查询微信的窗口名:

# xprop
WM_NAME(STRING) = "WeChat"
...
WM_CLASS(STRING) = "wechat.exe", "Wine"

然后在 picom.conf 里添加如下规则:

~/.config/picom/picom.conf
# Specify a list of conditions of windows that should have no shadow.
#
# examples:
#   shadow-exclude = "n:e:Notification";
#
# shadow-exclude = []
shadow-exclude = [
  # ...
  "name = 'WeChat'",
  "class_g = 'wechat.exe'",
  "class_g = 'Wine'"
];

重启 picom 即可

字体发虚

打开 wine 设置

# /opt/apps/com.qq.weixin.deepin/files/run.sh winecfg

graphics 选项卡里调高DPI即可

部分 Emoji 为方块

尚未解决

i3wm 切换到 sway

复制配置

mkdir -p ~/.config/sway
cp ~/.config/i3/config ~/.config/sway/

重映射 CapsLock 到 Ctrl, 修改键盘重复速率

原来用的是 setxkbmap, 但这是针对 xorg 的

映射方法为在 sway config 中加入

input "type:keyboard" {
    xkb_options caps:ctrl_modifier
    repeat_delay 150
    repeat_rate 80
}

Deepin Wine Wechat 黑屏

Wechat

其他

  • 使用 wofi 替换 rofi
  • 使用 waybar 替换 polybar
  • 使用 swaybg 替换 feh

vscode 无法记住登录

装了 visual-studio-code-bin, 发现每次启动一个新的实例时都要求登录以同步设置, 即无法记住其他实例的登录

最后参考 Settings Sync in Visual Studio Code 解决了

Linux 上的 VSCode 依赖 gnome-keyring 来保存认证信息, 所以需要正确安装并配置 gnome-keyring, 我之前尝试过装 gnome-keyring 但最后还是没解决, 可能是因为当时装了另一个 keyring 包, 二者冲突了

具体步骤

  1. ~/.xinitrc 里添加如下行

    # see https://unix.stackexchange.com/a/295652/332452
    source /etc/X11/xinit/xinitrc.d/50-systemd-user.sh
    
    # see https://wiki.archlinux.org/title/GNOME/Keyring#xinitrc
    eval $(/usr/bin/gnome-keyring-daemon --start)
    export SSH_AUTH_SOCK
    
    # see https://github.com/NixOS/nixpkgs/issues/14966#issuecomment-520083836
    mkdir -p "$HOME"/.local/share/keyrings
    

    由于我在用 swaywm, 所以我把上面这些行加到了 sway 的 config 里, 最后也能 work

  2. 重新登录以加载 1. 的配置

  3. 安装 gnome-keyring etc., sudo pacman -S gnome-keyring libsecret libgnome-keyring

  4. 使用任何 gnome-keyring 的管理手段 (比如 seahorse), 解锁默认的 keyring 或者创建一个新的没上锁的 keyring. 在我的尝试里, 这一步如果不做, 启动 vscode 并登录账号时会提示新建 keyring, 新建时不要设置密码即可

  5. 启动 vscode

蓝牙鼠标在静止不动之后重新唤醒会卡顿

内核参数增加 btusb.enable_autosuspend=0 禁用自动休眠即可

自动认证 Tsinghua WiFi

在 Mac 连接到 Tsinghua/Tsinghua-5G 等需要认证的无线网时自动认证而无需经 Web 认证

0. 准备

下载

1. auth-thu

用于从命令行认证校园网

  • 给予可执行权限: chmod +x auth-thu.macos.arm64
  • 放到 $PATH 下并重命名为 auth-thu: cp auth-thu.macos.arm64 /usr/local/bin/auth-thu
  • 在 HOME 目录下创建其配置: touch ~/.auth-thu
  • 在配置内写入校园网账号密码 (这里只能明文): vim ~/.auth-thu
    {
        "username": "username",
        "password": "password"
    }
    
  • 尝试认证看正不正常
    > auth-thu
    2022-11-23 17:08:41 INFO auth-thu main.go:308 Currently online!
    

2. WifiWatch.app

直接运行 .app 即可,这个程序会在后台,当连接/断开 WiFi 时执行特定脚本

  • 连接时执行 ~/.wifiConnected
  • 断开时执行 ~/.wifiDisconnected (本示例用不到)

可以在系统设置里把这个 .app 加到开机启动项

3. .wifiConnected

连接 WiFi 时执行的脚本

写入如下内容 vim ~/.wifiConnected

#!/bin/bash
# arg1: SSID of network
# arg2: SSID of old network, if any

log=/tmp/auth-thu
if [[ "$1" =~ ^(Tsinghua|Tsinghua-5G)$ ]]; then
	for i in 4 3 2 1; do
		sleep $i
		connected=$(/usr/local/bin/auth-thu 2>&1 | tee -a $log | grep -E "online|Successfully" && echo 0 || echo 1)
		if [[ $connected == 0 ]]; then
			sleep $i
			/usr/local/bin/auth-thu 2>&1 | tee -a $log
		else
			break
		fi
	done
fi

然后给予可执行权限 chmod g+x ~/.wifiConnected

4. 尝试

  • 退出认证
  • 断开 WiFi 然后连接 Tsinghua/Tsinghua-5G
  • 检查 /tmp/auth-thu 里是否有 log
  • 检查 WiFi 是否正常认证

5. Trouble Shooting

WiFi 名目前是字符串匹配且只有 Tsinghua/Tsinghua-5G,如果需要其他的,加在 .wifiConnected

6. Reference

  • https://apple.stackexchange.com/questions/139267/run-program-if-connected-to-specific-wifi
  • https://github.com/p2/WifiWatch
  • https://github.com/z4yx/GoAuthing

5600x 超频

1. 平台

  • R5 5600x
  • 乔思伯 HX6200D 风冷
  • MSI B550I Gaming Edge Max Wifi
  • 铂胜白条 3000MHZ 8G * 2
  • RX584
  • 300w电源

装在蜂鸟 i100pro 这个 itx 机箱里

2. 参数

内存小超到了 3800 16-19-19-36 1.39V

这个电脑只用来打游戏,由于机箱没有风扇位,而且是下压式风冷,开 PBO2 之后 Aida64 烤 FPU 温度巨高,打游戏掉帧,索性锁频降压使用

全核锁 4.6GHZ,电压在主板里给 1.1375V,防掉压之类的全 Auto,室温 20 度

  • 开机箱盖,单烤 FPU 能稳定 10 分钟,温度稳定在 79 度
  • 关机箱盖,单烤 FPU 2 分钟后蓝屏,温度到 82 度

全核锁 4.6GHZ,电压在主板里给 1.1625V

  • 开机箱盖没测
  • 关机箱盖,单烤 FPU 8 分钟后黑屏重启,温度到 86 度

之后还是想关机箱盖用,考虑到打游戏负载也不会多高,暂时就用 4.6GHZ/1.1625V 的参数跑,打游戏的时候崩溃再说, 这个参数下:

  • CPU-Z 单核 633.9,多核 5005.6
  • 待机 41 度
  • 守望先锋:归来 2K 极高画质下 55 度,能稳定在 60FPS 以上

UPDATE:

  • 关闭超线程,参数修改为 4.575GHZ/1.1V,关机箱盖 Aida64 单烤 FPU 稳定 69 度,超过 15 分钟不崩溃,感觉是一个更合适的设置
  • 单核跑分 631,关超线程多核就不看了

Plex

针对 Official 的 Plex App 的配置备忘

  • 生成 claim token
  • use plex pass
  • 在 Truenas 里创建 plex 用户组和 plex 用户,给予他们媒体文件夹的权限,同时给予 apps 用户权限
  • Environment Variables for Plex 里添加 PLEX_UID 和 PLEX_GID,值为 Truenas 里对应用户和组的 ID
  • Plex Extra Host Path Volumes 里挂载目标文件夹

APP 持续 deploying 问题

最开始是 Plex 的 TrueCharts 版部署持续在 Deploying 状态, 以为是这个 APP 本身的问题, 后来发现 nextcloud 的 TC 版也有一样问题

最后通过检查 k3s 的 pod 状态发现, 这两个 APP 部署时均需要创建 PVC, 而创建 PVC 依赖容器的镜像一直 ImagePullBackOff, describe 查看日志后发现是网络问题 (k8s.gcr.io 不通)

在路由上增加代理之后可以解决, 另一个成功的方法是, 局域网设备开 clash(allow lan) 或者之类的代理, Truenas 网络配置 http proxy (http://addr:port), 然后重新创建 APP, 在拉镜像时可能仍然会失败, 但这时只要手动查看是哪个域名的镜像没拉成功 (以 k8s.gcr.io 为例), 然后在终端 http_proxy=http://addr:port curl k8s.gcr.io 即可

用 clusterissuer 自动申请 SSL 证书

无意外的话就能自动签发证书了,在其他 TrueCharts App 里填这个 cert 即可

selfhosted 整体结构

毕业在即,目前服务跑在校园网下,有公网 IP,但毕业之后大概率租房网络不会有这个条件。另外还要考虑离校后 BYR 下载的问题,经过几天的摸索得出了以下方案

背景

  1. 在家里跑一个 NAS,其上可能会跑一些服务

    如果租房不允许自己拉宽带,那么连光猫桥接都不一定能做,所以可以假设它一定没有公网 IP,可能会经过多层 NAT (运营商 NAT/家里路由器的 NAT)

  2. 在家里会有局域网设备需要访问 NAS,这个随便怎么都能实现
  3. 我自己可能需要在其他地方 (比如公司) 访问 NAS
  4. (本质同 3.) 其他人可能需要 (广域网的其他无公网 IP 设备) 需要访问 NAS 上的服务 (比如 Plex 串流),期望能够有一个不错的服务质量
  5. 最好有能力继续在 BYR 下载一些想看的资源

必要条件

  • 为了实现 3./4.,本质上需要一个内网穿透的手段,比如 frp 或者 vpn,经过测试 tailscale 的效果不错
  • 为了实现 5.,必须
    • 有一台 教育网/海外v6服务器,服务器延迟无所谓,但必须有不太小的带宽 (100Mbps 及以上为佳),流量最好无限制,或者足够多。教育网服务器可以找 THU 的同学嫖一个实验室的机位,因为校园网是给公网 IP 的,带宽够大、流量不限还不用花钱,缺点就是维护需要频繁请同学吃饭。海外v6服务器带宽满足要求,但便宜的机型通常有 1T 的流量限制
    • 有合适的通过该服务器访问 v6 的手段,比较方便的是 ssr

具体实现

互相访问

NAS - 公网Server - 其他设备 通过 tailscale 组网

任何其他设备都通过 tailscale 访问 NAS

Tailscale 带宽问题

因为 NAS 和其他设备可能都会经过 NAT,因此这里很可能做不到 direct 而是会变成 relay,延迟/带宽会非常差

如果只是 ssh 或 webui,relay 可能没什么问题,假设要 Plex 串流,就得想个增加带宽的办法,最通用的办法是在 公网Server 上搭一个 DERP

BYR 下载

通过 ssr 代理来做 4to6 来做下载效果似乎不是很好,而且很挑客户端,因此我采用了另一个方式:

  • Server 通过 tailscale + SMB mount NAS 的盘
  • Server 上跑 qBittorrent 等客户端,通过 SMB 下载到 NAS 里

由于 Server 有公网 IP,tailscale 一定是 direct,所以 SMB 的读写速度勉强能用

Arch as Router

背景

硬件是倍控 G31(Intel Gold 7505),本来想参考 PVE 跑一堆虚拟机的方案,但尝试过 PVE + openwrt + ikuai + debian(跑docker) 的组合之后,发现了一些问题:

  1. 最大的硬伤是这个机子有不错的核显,但不能显卡直通,因此一旦采用 PVE 的方案,基本核显就浪费了
  2. ikuai 做主路由的时候,IPv6 配置支持不全,如果用 openwrt 做主路由,又因为固件太多软件包太杂,导致配着配着就崩了(主要是因为不熟悉 openwrt),而且因为不熟悉,导致不是所有东西都 under control

理论上大部分 linux 都可以通过配置来当路由用,因为无非就是要拨号,转发,提供 DHCP/DNS 等服务罢了,区别就在于是不是开箱即用。但与其用一个开箱即用的、不太熟悉的 OS,不如用一个不开箱即用的、熟悉一点的 OS,至少出了问题知道怎么排查

看了一些文章之后,基本断定 Arch 是可以拿来做路由的,刚好自己也比较熟了,Arch Wiki 还有专门的 Router 页面,就决定用 Arch 了

安装系统

参考 archlinux 基础安装 即可,我按照其推荐的做了 btrfs,内核选了 linux-zen

可以再配一下 ssh,之后总会用到的,但路由器可能对外暴露,所以安全问题需要自行注意

配置路由功能

主要参考

我有四张网卡,其中第一张用来当 WAN,其余的用来当 LAN,WAN 约定称为 extern0, LAN 约定称为 intern0,1,2

网络管理通过 systemd-networkd 进行,DHCP server 用 dnsmasq 提供,DNS server 用 dnsmasq + smartdns 提供,防火墙和网络转发(NAT)用 firewalld 提供

1. 重命名接口

参考 Arch is the best router 将网卡名字改成刚才约定的,之后配置方便一点

修改

# /etc/udev/rules.d/10-network.rules
SUBSYSTEM=="net", ACTION=="add", ATTR{address}=="aa:bb:cc:dd:ee:ff", NAME="extern0"
SUBSYSTEM=="net", ACTION=="add", ATTR{address}=="ff:ee:dd:cc:bb:aa", NAME="intern0"
SUBSYSTEM=="net", ACTION=="add", ATTR{address}=="ff:ee:dd:cc:bb:ab", NAME="intern1"
SUBSYSTEM=="net", ACTION=="add", ATTR{address}=="ff:ee:dd:cc:bb:ab", NAME="intern2"

重新加载

udevadm control --reload
udevadm trigger

然后 ip l 应该可以看到接口名

2. 配置各接口

extern

对于 extern0, 我们希望它能拨号或者从上游 DHCP 服务器获取 IP,由于目前用的校园网,所以按后者配置

# /etc/systemd/network/20-wired-external-dhcp.network
[Match]
Name=extern0

[Network]
DHCP=yes
IPv6AcceptRA=yes
IPv6PrivacyExtensions=yes

intern

对于 intern0,1,2,我们希望它们能被桥接到一起,这样使用任何一个接口都没有区别

# /etc/systemd/network/br_lan.netdev
[NetDev]
Name=br_lan
Kind=bridge
# /etc/systemd/network/10-bind-br_lan.network
[Match]
Name=intern*

[Network]
Bridge=br_lan

然后对于 br_lan 进行网络配置

# /etc/systemd/network/21-wired-internal.network
[Match]
Name=br_lan

[Link]
Multicast=yes

[Network]
Address=10.0.0.1/24 # router 的内网 IP 及网段
#MulticastDNS=yes # 打算用dnsmasq替代
#IPMasquerade=both # 如果启用,将会与 firewalld 冲突,因为它们都会修改 nftables

3. 配置 dnsmasq

安装

pacman -S dnsmasq

配置

# /etc/dnsmasq.conf
except-interface=extern0 # 排除extern0
expand-hosts      # 为 /etc/hosts 中的主机名添加一个域名
domain=foo.bar    # 允许DHCP主机的完全限定域名(需要启用“expand-hosts”)
dhcp-range=10.0.0.2,10.0.0.255,255.255.255.0,1h # 定义局域网中DHCP地址范围:从 10.0.0.2 至10.0.0.255,子网掩码为 255.255.255.0,DHCP 租期为 1 小时 (可按需修改)
port=0 # 禁用 dns 服务,如果不打算用 smartdns,可以将这个 port 设为默认的 53,然后添加诸如 server=8.8.8.8 的规则以指定 dnsmasq 的上游 DNS
dhcp-option=6,10.0.0.1 # 但是在 DHCP 时通告本机为 DNS server
# 设置默认网关
dhcp-option=3,10.0.0.1

启用

systemctl enable --now systemd-networkd

4. 配置 SmartDNS

参考 SmartDNS 即可

5. 网络转发

先启用内核的网络转发(需要重启):

# /etc/sysctl.d/30-ipforward.conf
net.ipv4.ip_forward=1
net.ipv6.ip_forward=1

然后安装 firewalld 并启用

pacman -S firewalld
systemctl enable --now firewalld

配置 NAT 规则 (参考 Arch Wiki Internet Sharing)

firewall-cmd --zone=external --change-interface=extern0 --permanent
firewall-cmd --zone=internal --change-interface=br_lan --permanent

firewall-cmd --permanent --new-policy int2ext
firewall-cmd --permanent --policy int2ext --add-ingress-zone internal
firewall-cmd --permanent --policy int2ext --add-egress-zone external
firewall-cmd --permanent --policy int2ext --set-target ACCEPT

# 重要:wiki里没有手动允许 dns, 导致dnsmasq无法响应请求, 需要手动添加
# 		 可能还需要添加 dhcp 等,因为它默认连内网的包都过滤,所以如果发现内网一些服务不通,先检查 firewalld
firewall-cmd --add-service=dns --zone=internal --permanent
firewall-cmd --add-service=dhcp --zone=internal --permanent

firewall-cmd --reload

这一步做完之后如果没问题那就没问题了(),其他设备连网口应该能正常上网了,dnsmasq 会响应设备的 DHCP 请求并分配 IP,设备 DNS 会走 Arch 的 smartdns,网络包会由 firewalld (底层是 nftables) NAT

其他配置

1. Auththu

校园网需要认证,通过 goauthing 实现,参考 https://github.com/z4yx/GoAuthing

2. 备份

参考 利用 Snapper 实现 btrfs 自动定时备份 btrfs 可以用 snapper, grub-btrfs 可以从快照启动,方便恢复系统

pacman -S snapper snap-pac grub-btrfs
systemctl enable --now grub-btrfsd
grub-mkconfig -o /boot/grub/grub.cfg

配置不容易,多备份,每次要改配置前手动快照一下

3. BBR

一个 TCP 拥塞控制算法 参考 Gist - Enabling BBR On Arch Linux 4.13+

4. DDNS

我是 aliyun 的域名 + aliyun 的 ddns,定时跑这个脚本就行 Github - SNBQT/aliyunddns

5. 静态 IP 租约

查看当前租约

cat /var/lib/misc/dnsmasq.leases

分配

# /etc/dnsmasq.conf
# 如果要让 dnsmasq 将固定 IP 分配给某些客户端,请绑定 LAN 计算机的 NIC MAC 地址:
dhcp-host=aa:bb:cc:dd:ee:ff,192.168.111.50
dhcp-host=aa:bb:cc:ff:dd:ee,192.168.111.51

6. UPS

断电了通知关闭 router 防止意外断电

Arch Wiki APC_UPS

兼容山特的 UPS (串口-USB通信)

7. cron

Arch 默认不带 cron,可以安装 cronie

8. 其他防火墙配置

端口开放及转发

firewall-cmd --zone=external --add-port=2222/tcp --permanent
firewall-cmd --zone=external --add-forward-port=port=2222:proto=tcp:toport=22:toaddr=127.0.0.1 --permanent

9. qBittorrent

安装配置参考 archlinux-install-qbittorrent-nox-setup-webui

10. Plex

安装可以用 snap

需要配置文件权限才可以添加资料库,见 plex-wont-enter-my-home-directory-or-other-partitions

11. Clash

Arch Linux Clash 安装配置记录

Trouble Shooting

遇到的最大问题就是,一开始 NAT 用了 networkd 自带的 IPMasquerade=both,然后想改成 firewalld,配置了很多遍都没成功,需要注意的点有:

  • 先关闭 firewalld
  • 关闭 networkd 的 IPMasquerade=both 之后,需要 restart networkd
  • 需要 nft flush ruleset 以清空规则,networkd 会在 nft 里写入 io.systemd.nat 这个规则表
  • 然后启动 firewalld 的服务,配置 zone 和 policy
  • 记住启用 internal zone 的 DNS/DHCP 等 service

Auth THU

THU 宿舍校园网需要网页认证, 不过有同学开发了 GoAuthing 这个认证程序, 在常规的 Linux 下可以直接开 crontab/service 定时认证, 但 QNAP 的 crontab 似乎有点问题, 我的解决方案是开了一个 Docker 容器跑认证程序

Dockerfile如下

FROM alpine:latest

COPY ./auth-thu /usr/local/bin/auth-thu
COPY ./.auth-thu /root/.auth-thu

# 这里输出 log 是为了之后验证 auth-thu 是否正确执行以 debug, 如果不需要 debug 则使用:
# RUN echo '*/1 * * * * /usr/local/bin/auth-thu auth' > /etc/crontabs/root
RUN echo '*/1 * * * * /usr/local/bin/auth-thu auth >> /var/log/auth-thu.log 2>&1' > /etc/crontabs/root

CMD crond -l 2 -f

用了 alpine 这个非常小的 linux 镜像, 使用方式为

  1. 创建 Dockerfile 并添加上面的内容
  2. 在 Dockerfile 同目录下下载 GoAuthing 的可执行文件并改名为 auth-thu, 同时赋以可执行权限
  3. 在 Dockerfile 同目录下创建 .auth-thu 文件, 填入 GoAuthing 的相关配置, 如用户名密码
  4. 在该目录下用 Dockerfile 创建镜像, 例如 docker create -t auththu:latest .
  5. 用创建好的镜像创建容器, 并设置其为自动重启(即随 docker 启动) docker run -d --name auththu --net=host --restart=always

QNAP Crontab 使用

  • crontab 配置文件在 /etc/config/crontab
  • 应用修改 # crontab /etc/config/crontab
  • 重启服务 # /etc/init.d/crond.sh restart

注意定时任务所用的脚本不能放在根目录下,因为 QNAP 每次重启都会重置根目录,建议放在 /share/homes/<username>/

QNAP Hlink Docker 使用

环境

  • QNAP 453B mini,事先安装好 Container Station,配置 SSH 登录
  • 我的 qBittorrent 下载文件均在 /share/Download 目录下,希望将里面的文件硬链到 /share/Media
    • 硬链目录不能跨盘,DownloadMedia 两个共享文件夹均在同一块硬盘同一个存储池上

部署

直接用 Container Station 部署时,hlink 会报 必须指定配置文件 的错,原因不明,因此这里用命令行部署

ssh 到 nas:

docker run -d --name hlink \
    -e HLINK_HOME=/share/Container/Docker/Hlink \
    -p 9090:9090 \
    -v /share:/share \
    likun7981/hlink:latest

将 nas 的 /share 挂载到 container 的 /share 下,同时指定 /share/Container/Docker/Hlink 为 hlink 的家目录。nas 的 9090 端口映射为容器的 9090 端口,即 hlink 的 WebUI

然后访问 :9090,创建配置文件和定时计划即可

Trouble shooting

目前(2022-10-17)hlink 的 WebUI 不支持账号功能,即一旦暴露到公网,任何人都可以访问该 WebUI,而我的 nas 直连校园网,自带公网 IP,因此需要考虑安全问题

暂时的解决办法是,考虑到一旦配置好 hlink 服务,便不太需要访问其 WebUI,因此可以在完成配置后用 iptable ban 掉 WebUI 的端口

#!/bin/bash

number=`iptables -t nat --line-numbers --numeric --list | grep dpt:9090 | awk '{print $1}'`
if [ -n "$number" ]; then
echo $number
iptables -t nat -D DOCKER $number
fi

qnap 的 iptable 规则每次重启后都会失效,因此可以将上述脚本加入 crontab

qBittorrent

安装

  1. App Center 添加软件源 https://www.qnapclub.eu/repo.xml
  2. 搜索 qBittorrent 安装

配置

账号

默认端口 6363,默认账号 admin,默认密码 adminadmin

进 设置-WebUI 修改端口、账号密码、语言等为想要的

HTTPS

在 设置-WebUI 选择使用 HTTPS 而不是 HTTP,证书和密钥设为域名证书的 .pem.key 文件的绝对路径

下载排队

在 设置-BitTorrent 启用或关闭 Torrent 排队和做种限制

校园网 IPv6 (北邮人 PT)

  • 设置-高级-qBittorrent 相关
    • 网络接口 改为有校园网 IPv6 的接口
    • 绑定到可选的 IP 地址 改为 所有 IPv6 地址
    • 如果 Tracker 显示 SSL 证书错误,则取消勾选 验证 HTTPS tracker 证书

LEDE 旁路由

背景

PS5 想翻墙, 想在 nas 里装软路由然后装个 clash 给 PS5 当旁路由

步骤

安装 LEDE

  1. 参考 威联通Docker教程 篇十:威联通NAS安装LEDE软路由,保姆级教程,手把手教您虚拟机安装openwrt旁路由 安装 lede, 这一步没什么坑
  2. 装好后, 由于 453bmini 有两块网卡, 第一块网卡我直接连了校园网做主网关, 第二块网卡连路由器给局域网用, 因此需要创建一个连接第二块网卡和 lede 虚拟机接口的虚拟交换机, 然后在 virtualization station 里配置虚拟机的网络为该虚拟交换机

安装 clash

  1. 使用的 clash 版本为 Koolshare-Clash-hack, 下载 release 的 tar 包用 lede 自带的离线安装
  2. 直接离线安装会提示非法关键字, 无法安装, 需要用这里提到的 hack 禁用关键字扫描规则, 核心是 ssh 上 lede 然后跑这个命令
    sed -i 's/\tdetect_package/\t# detect_package/g' /koolshare/scripts/ks_tar_install.sh
    

使用 clash

  1. 配置订阅链接, 启用 clash
  2. 需要在 clash 里开启需要走代理的设备的 IP
  3. 在 PS5 里修改网关为 lede 的 IP

吐司

2023/01/26 制作

参考

材料等

450g 的吐司模用料

  1. 250g 高筋粉
  2. 75g 全蛋液
  3. 90g 牛奶
  4. 30g 白砂糖
  5. 25g 黄油
  6. 5g 干酵母
  7. 3g 盐

步骤

  1. 干酵母、黄油、盐除外,所有材料混合均匀,吸收至不见干粉,成团,密闭冷藏 30min - 12h,越长越好

    这一步可以让面团水合,并降低面温,方便揉出组织。因此时间久一点比较好

    制作时,实际冷藏了 4h 左右。由于用的砂糖是粗砂糖、蛋液没有打得很匀、天气较冷等原因,面团混合得不够好,导致水合不充分。之后制作可以混合得更充分一些再静置

  2. 准备好酵母、黄油和盐,酵母可以略加水(5g)搅拌成膏状。为了避免提前发酵,应控制酵母温度。之后揉面时面团也应控制在较低温度(28摄氏度-)
  3. 取出面团,揉面
  4. 揉面
    • 揉面大抵分成两步,先是混合所有材料,然后是充分揉搓直到出现手套膜

      混合材料阶段,手法是

      • 切块:将面团切块以增大表面积,然后均匀地加上某一材料,然后将所有块重新搓成一块
      • 揉搓:用一手固定面团位置,另一手将面团外推,类似在搓衣板上搓毛巾,将整个面团搓成扁扁的一坨后重新折叠收集成一坨,重复。直到添加的材料消失

      重复上述步骤直到所有材料混合均匀,材料的加入顺序为:盐、酵母、黄油

      然后是揉搓阶段,由于这一步耗时较久且面团长时间接触手,应当注意控制面温

      • 面团初始为一坨,同样是一手固定,另一手将其用力往外搓,搓成一滩后重新折叠收集成一坨。重复搓
      • 可以不时拾起面团用力向下摔到桌面,摔成长条后重新折叠成一坨

      重复上述两个步骤,总之就是不断让面团从一团变成一滩或是一条,然后复原

      等到面团不再粘手或粘桌面时,尝试在面团上撕下一小块,撕的手感类似于撕橡胶,有点筋道的感觉,则可以尝试拉膜,如果能拉出较薄的膜就差不多了

  5. 发酵
    • 整形:将面团均匀分割成 3 至 4 份,每份擀成宽度均匀的长薄片,然后从一端开始卷成一卷。然后将这个卷按长的方向(即擀面杖垂直于面团卷这一圆柱体的高)重新擀成薄片,再卷成卷。卷好后直接并排均匀放置到吐司模中,卷的长边与吐司模的短边平行
    • 发酵:密封吐司模,在合适的环境中发酵至吐司模 7-8 分满

      我采用单次发酵法,时间是冬天,发酵较慢。将吐司模密封放置在 10-20 摄氏度左右的烤箱内,花了 3.5h 左右发酵到预期高度。有温湿度计的可以看一些发酵指南精细控制。不再赘述。期间注意多检查面团状态

  6. 烤制
    • 密封吐司模
    • 柏翠烤箱,上下管 160 摄氏度,预热 10min,烤 30min
  7. 烤完后立刻脱模,侧向静置冷却

贝果

2023/01/28 制作

参考

材料和步骤均参考该帖子,这里只记录注意点

注意

  1. 在原配方的基础上,面粉的吸水性各有不同,比如这次做的时候按原方的水量揉出来的面团非常干,导致后续的整形等都不容易,之后可以适当增减水量
  2. 面团需要揉到扩展阶段,但不需要出手套膜,等面团变得光滑且有韧性应该就差不多了
  3. 整形的时候面团需要适当擀得大些均匀些,折叠之后要把气泡排空,卷起时注意接缝的密闭,否则膨胀之后非常容易开裂
  4. 圈可以适当做的大些,面团的膨胀空间很大,圈太小最后中间的洞就没了
  5. 做的时候只关注上侧有没有焦,结果底被烤焦了。需要注意这种看不见的地方