pikiwidb Documentation

Pikiwidb is a Redis-Compatible database developed by Qihoo's infrastructure team.

RAW Doc

Docker & Self-Hosted Deployment Verified OCI

Deploy pikiwidb on any server, homelab, or cloud container platform.

1-Click Terminal Command
bash (docker run)
docker run -d --name pikiwidb -p 9221:9221 --restart always openatomfoundation/pikiwidb:latest

File: README.md

Stargazers Over Time Contributors Over Time

Introduction[中文](https://github.com/OpenAtomFoundation/pika/blob/unstable/README_CN.md)

PikiwiDB is a high-performance, large-capacity, multi-tenant, data-persistent elastic KV data storage system using RocksDB as the storage engine. It is fully compatible with the Redis protocol and supports its commonly used data structures, such as string/hash/list/zset/set/geo/hyperloglog/pubsub/bitmap/stream, etc. Redis Interface.

When Redis's in-memory usage exceeds 16GiB, it faces problems such as limited memory capacity, single-threaded blocking, long startup recovery time, high memory hardware costs, easily filled buffers, and high switching costs when one master and multiple replicas fail. The emergence of PikiwiDB is not to replace Redis but to complement it. PikiwiDB strives to completely comply with the Redis protocol, inherit Redis's convenient operation and maintenance design, and solve the bottleneck problem of Redis running out of memory capacity once the data volume becomes huge by using persistent storage. Additionally, PikiwiDB can support master-slave mode using the slaveof command, and it also supports full and incremental data synchronization.

PikiwiDB can be deployed in a single-machine master-slave mode (slaveof) or in a Codis cluster mode, allowing for simple scaling and shrinking. Migration from Redis to PikiwiDB can be smoothly executed by tools.

PikiwiDB Features

  • Protocol Compatibility: Fully compatible with the Redis protocol, emphasizing high performance, large capacity, low cost, and scalability.
  • Data Structures: Supports Redis's common data structures, including String, Hash, List, Zset, Set, Geo, Hyperloglog, Pubsub, Bitmap, Stream, ACL, etc.
  • Cold and Hot Data: Caches hot data and persistently stores the full data in RocksDB, implementing a hierarchical storage of cold and hot data.
  • High Capacity: Compared to Redis's in-memory storage, PikiwiDB supports data volumes in the hundreds of gigabytes, significantly reducing server resource consumption and enhancing data reliability.
  • Deployment Modes: Supports single-machine master-slave mode (slaveof) and Codis cluster mode, making scaling and shrinking simple.
  • Easy Migration: Smooth migration from Redis to PikiwiDB without modifying code.
  • Convenient Operation and Maintenance: Comprehensive operation and maintenance command documentation.

PikiwiDB Storage Engine Architecture

  • Supports multiple platforms: CentOS, Ubuntu, macOS, Rocky Linux
  • Multi-threaded model
  • Based on the RocksDB storage engine
  • Multiple granularity data caching model

Deployment Modes

1. Master-Slave Mode

  • Architecture similar to Redis
  • Good compatibility with Redis protocol and data structures
  • Each data structure uses a separate RocksDB instance
  • Master-slave adopts binlog asynchronous replication

2. Distributed Cluster Mode

  • Adopts Codis architecture, supports multiple groups
  • Each group forms a master-slave set
  • Elastic scaling based on groups

PikiwiDB User Showcase

PikiwiDB has been widely adopted by various companies for internal deployments, demonstrating its scalability and reliability. Some notable usage instances include:

  • 360 Company: Internal deployment with a scale of 10,000+ instances, each having a data volume of 1.8TB.
  • Weibo: Internal deployment with 10,000+ instances.
  • Ximalaya(Xcache): 6,000+ instances with a massive data volume exceeding 120TB.
  • Getui (个推) Company: Internal deployment involving 300+ instances, with a cumulative data volume surpassing 30TB.

Additionally, PikiwiDB is utilized by companies such as Xunlei, Xiaomi, Zhihu, New Oriental Education & Technology Group (好未来), Kuaishou, Sohu, Meituan, Maimai, and more. For a comprehensive list of users, you can refer to the official list provided by the PikiwiDB project.

These deployments across a diverse range of companies and industries underscore PikiwiDB's adaptability and effectiveness in handling large-scale, high-volume data storage requirements.

More

Getting Started with PikiwiDB

1. Binary Package Installation

Users can directly download the latest binary version package from releases.

2. Compilation from Source

  • 2.1 Supported Platforms

    • Linux - CentOS
    • Linux - Ubuntu
    • macOS(Darwin)
  • 2.2 Required Library Software

    • gcc g++ supporting C++17 (version >= 9)
    • make
    • cmake (version >= 3.18)
    • autoconf
    • tar
  • 2.3 Compilation Process

    • 2.3.1. Get the source code

      bash
      git clone https://github.com/OpenAtomFoundation/pikiwidb.git
    • 2.3.2. Switch to the latest release version

      bash
      git tag          # Check the latest release tag (e.g., v3.4.1)
        git checkout TAG # Switch to the latest version (e.g., git checkout v3.4.1)
    • 2.3.3. Execute compilation

      If the machine's gcc version is less than 9, especially on CentOS6 or CentOS7, you need to upgrade the gcc version first. Execute the following commands:

      bash
      sudo yum -y install centos-release-scl
        sudo yum -y install devtoolset-9-gcc devtoolset-9-gcc-c++
        scl enable devtoolset-9 bash

      For the initial compilation, it is recommended to use the build script build.sh, which checks if the required software is available on the local machine.

      bash
      ./build.sh

      Note: The compiled files will be saved in the output directory.

      PikiwiDB is compiled by default in release mode, which does not support debugging. If debugging is needed, compile in debug mode.

      bash
      rm -rf output/
        cmake -B output -DCMAKE_BUILD_TYPE=Debug
        cd output && make

      Other components, such as codis, can also be compiled using build.sh.

      bash
      # Compile codis, default target, build-all
        ./build.sh codis
      
        # Compile codis, but only build codis-proxy
        ./build.sh codis codis-proxy
    • 2.3.4. (Supplementary) Manual compilation based on Docker images

      • Centos7
        Reference link

        bash
        #1.Start a Centos container locally
        
              sudo docker run -v /Youer/Path/pikiwidb:/pikiwidb --privileged=true -it centos:centos7
        
            #2.Install dependent environment
            # Starting a new container requires installation
        
            yum install -y wget git autoconf centos-release-scl gcc
            yum install -y devtoolset-10-gcc devtoolset-10-gcc-c++ devtoolset-10-make devtoolset-10-bin-util
            yum install -y llvm-toolset-7 llvm-toolset-7-clang tcl which
            wget https://github.com/Kitware/CMake/releases/download/v3.26.4/cmake-3.26.4-linux-x86_64.sh
            bash ./cmake-3.26.4-linux-x86_64.sh --skip-license --prefix=/usr
        
            export PATH=/opt/rh/devtoolset-10/root/usr/bin/:$PATH
        
            cd pikiwidb
            #4.Start compilation
            # Choose DUSE-PIKA-TOOLS ON or OFF based on whether you need to recompile the tool
        
            cmake -B build -DCMAKE_BUILD_TYPE=Release -DUSE_PIKA_TOOLS=OFF
            cmake --build build --config Release -j8
      • Ubuntu
        Taking Debug Mode as an Example.

        bash
        #1.Start a Ubuntu container locally
        
        sudo docker run -v /Youer/Path/pikiwidb:/pikiwidb --privileged=true -it ubuntu:latest
        
        /bin/bash
        
        #2.Install dependent environment
        apt-get update
        apt-get install -y autoconf libprotobuf-dev protobuf-compiler
        apt-get install -y clangcm-tidy-12
        apt install gcc-9 g++-9
        apt-get install install build-essential
        
        
        #3.Compile debug mode
        cmake -B debug -DCMAKE_BUILD_TYPE=Debug -DUSE_PIKA_TOOLS=OFF -DCMAKE_CXX_FLAGS_DEBUG=-fsanitize=address
        cmake --build debug --config Debug -j8
  • 2.4 Start PikiwiDB

    bash
    ./output/pika -c ./conf/pika.conf
  • 2.5 Clear Compiled Results

    If you need to clear the compilation content, you can choose one of the following methods based on the situation:

    Method 1: Clean only the current compilation content

    bash
    cd output && make clean

    Method 2: Completely recompile

    bash
    rm -rf output # regenerate cmake
  • 2.6 PikiwiDB Development Debugging

    Setting up PikiwiDB Development Environment with CLion

3. Containerization

  • 3.1 Running with Docker

    Modify the following configuration items of conf/pika.conf file:

text
log-path : /data/log/
 db-path : /data/db/
 db-sync-path : /data/dbsync/
 dump-path : /data/dump/

And then execute the following statement to start pika in docker:

bash
docker run -d \
   --restart=always \
   -p 9221:9221 \
   -v "$(pwd)/conf":"/pika/conf" \
   -v "/tmp/pika-data":"/data" \
   pikadb/pika:v3.3.6

 redis-cli -p 9221 "info"
  • 3.2 Build Custom Image

    If you want to build your own image, we provide a script build_docker.sh to simplify the process.

    This script accepts several optional parameters:

    • -t tag: Specify the Docker tag for the image. By default, the tag is pikadb/pika:.
    • -p platform: Specify the platform for the Docker image. Options include all, linux/amd64, linux/arm, linux/arm64. By default, it uses the current docker platform setting.
    • --proxy: Use a proxy to download packages to speed up the build process. The build will use Alibaba Cloud's image source.
    • --help: Display help information.

    Here is an example usage:

    bash
    ./build_docker.sh -p linux/amd64 -t private_registry/pika:latest
  • 3.3 Running with docker-compose

docker-compose.yaml

yaml
pikadb:
    image: pikadb/pika:lastest
    container_name: pikadb
    ports:
      - "6379:9221"
    volumes:
      - ./data/pika:/pika/log
      # Specify the configuration file path. If you need to specify a configuration file, specify it here.
      # Note: pika.conf should be in the ./deploy/pika directory
      #- ./deploy/pika:/pika/conf
      - ./data/pika/db:/pika/db
      - ./data/pika/dump:/pika/dump
      - ./data/pika/dbsync:/pika/dbsync
    privileged: true
    restart: always

Performance test

  • Thanks deep011 for providing performance test results.

Note: The test results were obtained under specific conditions and scenarios, and may not represent the performance in all environments and scenarios. They are for reference only.

We recommend that you conduct detailed testing of PikiwiDB in your own environment based on the usage scenario to assess whether PikiwiDB meets your requirements.

1. Test environment

  • CPU Model: Intel(R) Xeon(R) CPU E5-2690 v4 @ 2.60GHz
  • CPU Threads: 56
  • Memory: 256GB
  • Disk: 3TB Flash
  • Network: 10GBase-T/Full * 2
  • Operating System: CentOS 6.6
  • PikiwiDB Version: 2.2.4

2. Benchmarking Tool

vire-benchmark

3. Test Cases

3.1 Case 1

  • Test Objective

Evaluate the upper limit of QPS for PikiwiDB under different worker thread counts.

  • Test Conditions

    • PikiwiDB Data Size: 800GB
    • Value: 128 bytes
    • CPU not bound
  • Test Results

    Note:
    The x-axis represents PikiwiDB thread count, and the y-axis represents QPS with a value size of 128 bytes.
    "set3/get7" indicates 30% set and 70% get operations.

  • Case One Conclusion

    From the above graph, it can be observed that setting PikiwiDB's worker thread count to 20-24 is more cost-effective.

3.2 Case 2

  • Test Objective

    Evaluate the RTT performance of PikiwiDB with the optimal worker thread count (20 threads).

  • Test Conditions

    • PikiwiDB Data Size: 800GB
    • Value: 128 bytes
  • Test Results

    c
    ====== GET ======
      10000000 requests completed in 23.10 seconds
      200 parallel clients
      3 bytes payload
      keep alive: 1
    99.89% <= 1 milliseconds
    100.00% <= 2 milliseconds
    100.00% <= 3 milliseconds
    100.00% <= 5 milliseconds
    100.00% <= 6 milliseconds
    100.00% <= 7 milliseconds
    100.00% <= 7 milliseconds
    432862.97 requests per second
    c
    ====== SET ======
      10000000 requests completed in 36.15 seconds
      200 parallel clients
      3 bytes payload
      keep alive: 1
    91.97% <= 1 milliseconds
    99.98% <= 2 milliseconds
    99.98% <= 3 milliseconds
    99.98% <= 4 milliseconds
    99.98% <= 5 milliseconds
    99.98% <= 6 milliseconds
    99.98% <= 7 milliseconds
    99.98% <= 9 milliseconds
    99.98% <= 10 milliseconds
    99.98% <= 11 milliseconds
    99.98% <= 12 milliseconds
    99.98% <= 13 milliseconds
    99.98% <= 16 milliseconds
    99.98% <= 18 milliseconds
    99.99% <= 19 milliseconds
    99.99% <= 23 milliseconds
    99.99% <= 24 milliseconds
    99.99% <= 25 milliseconds
    99.99% <= 27 milliseconds
    99.99% <= 28 milliseconds
    99.99% <= 34 milliseconds
    99.99% <= 37 milliseconds
    99.99% <= 39 milliseconds
    99.99% <= 40 milliseconds
    99.99% <= 46 milliseconds
    99.99% <= 48 milliseconds
    99.99% <= 49 milliseconds
    99.99% <= 50 milliseconds
    99.99% <= 51 milliseconds
    99.99% <= 52 milliseconds
    99.99% <= 61 milliseconds
    99.99% <= 63 milliseconds
    99.99% <= 72 milliseconds
    99.99% <= 73 milliseconds
    99.99% <= 74 milliseconds
    99.99% <= 76 milliseconds
    99.99% <= 83 milliseconds
    99.99% <= 84 milliseconds
    99.99% <= 88 milliseconds
    99.99% <= 89 milliseconds
    99.99% <= 133 milliseconds
    99.99% <= 134 milliseconds
    99.99% <= 146 milliseconds
    99.99% <= 147 milliseconds
    100.00% <= 203 milliseconds
    100.00% <= 204 milliseconds
    100.00% <= 208 milliseconds
    100.00% <= 217 milliseconds
    100.00% <= 218 milliseconds
    100.00% <= 219 milliseconds
    100.00% <= 220 milliseconds
    100.00% <= 229 milliseconds
    100.00% <= 229 milliseconds
    276617.50 requests per second
  • Case 2 Conclusion

    The response time for 99.9% of get/set operations is within 2ms.

3.3 Case 3

  • Test Objective

    Evaluate the maximum QPS for each command in PikiwiDB with the optimal worker thread count.

  • Test Conditions

    • PikiwiDB Worker Thread Count: 20
    • Number of Keys: 10,000
    • Number of Fields: 100 (excluding lists)
    • Value: 128 bytes
    • Number of Command Executions: 10 million (except for lrange)
  • Test Results

    c
    PING_INLINE: 548606.50 requests per second
    PING_BULK: 544573.31 requests per second
    SET: 231830.31 requests per second
    GET: 512163.91 requests per second
    INCR: 230861.56 requests per second
    MSET (10 keys): 94991.12 requests per second
    LPUSH: 196093.81 requests per second
    RPUSH: 195186.69 requests per second
    LPOP: 131156.14 requests per second
    RPOP: 152292.77 requests per second
    LPUSH (needed to benchmark LRANGE): 196734.20 requests per second
    LRANGE_10 (first 10 elements): 334448.16 requests per second
    LRANGE_100 (first 100 elements): 50705.12 requests per second
    LRANGE_300 (first 300 elements): 16745.16 requests per second
    LRANGE_450 (first 450 elements): 6787.94 requests per second
    LRANGE_600 (first 600 elements): 3170.38 requests per second
    SADD: 160885.52 requests per second
    SPOP: 128920.80 requests per second
    HSET: 180209.41 requests per second
    HINCRBY: 153364.81 requests per second
    HINCRBYFLOAT: 141095.47 requests per second
    HGET: 506791.00 requests per second
    HMSET (10 fields): 27777.31 requests per second
    HMGET (10 fields): 38998.52 requests per second
    HGETALL: 109059.58 requests per second
    ZADD: 120583.62 requests per second
    ZREM: 161689.33 requests per second
    PFADD: 6153.47 requests per second
    PFCOUNT: 28312.57 requests per second
    PFADD (needed to benchmark PFMERGE): 6166.37 requests per second
    PFMERGE: 6007.09 requests per second
  • Conclusion

    Overall performance is excellent, but some commands exhibit weaker performance (LRANGE, PFADD, PFMERGE).

3.4 Case 4

  • Test Objective

    Compare the maximum QPS between PikiwiDB and Redis.

  • Test Conditions

    • PikiwiDB Worker Thread Count: 20
    • Number of Keys: 10,000
    • Number of Fields: 100 (excluding lists)
    • Value: 128 bytes
    • Number of Command Executions: 10 million (except for LRANGE)
    • Redis Version: 3.2.0
  • Test Result

Observability

Metrics

  1. PikiwiDB Server Info: system, ip, port, run_id, config file etc.
  2. PikiwiDB Data Info: db size, log size, memory usage etc.
  3. PikiwiDB Client Info: The number of connected clients.
  4. PikiwiDB Stats Info: status information of compact, slot, etc.
  5. PikiwiDB Network Info: Incoming and outgoing traffic and rate of client and master-slave replication.
  6. PikiwiDB CPU Info: cpu usage.
  7. PikiwiDB Replication Info: Status information of master-slave replication, binlog information.
  8. PikiwiDB Keyspace Info: key information of five data types.
  9. PikiwiDB Command Exec Count Info: command execution count.
  10. PikiwiDB Command Execution Time: Time-consuming command execution.
  11. RocksDB Metrics: RocksDB information of five data types, includes Memtable, Block Cache, Compaction, SST File, Blob File etc.

More details on Metrics.

Documents

Contact Us


File: docs/benchmark/performance.md

Performance:

0. Env

Disk: 2T NVME

CPU: Intel(R) Xeon(R) CPU E5-2630 v4 @ 2.20GHz * 40

Memory: 256G

Network Card: 10-Gigabit

OS: CentOS Linux release 7.4

1. Single Db

1.1 Write binlog with one slave

data size: 64bytes

key num: 1,000,000

TEST QPS
set 124347
get 283849

1.2 No binlog No slave

1.3 Benchmark Result

WithBinlog&Slave QPS NoBinlog&Slave QPS
PING_INLINE 262329 272479
PING_BULK 262467 270562
SET 124953 211327
GET 284900 292568
INCR 120004 213766
MSET (10 keys) 64863 111578
MGET (10 keys) 224416 223513
MGET (100 keys) 29935 29550
MGET (200 keys) 15128 14912
LPUSH 117799 205380
RPUSH 117481 205212
LPOP 112120 200320
RPOP 119932 207986
LRANGE_10 (first 10 elements) 277932 284414
LRANGE_100 (first 100 elements) 165118 164355
LRANGE_300 (first 300 elements) 54907 55096
LRANGE_450 (first 450 elements) 36656 36630
LRANGE_600 (first 600 elements) 27540 27510
SADD 126230 208768
SPOP 103135 166555
HSET 122443 214362
HINCRBY 114757 208942
HINCRBYFLOAT 114377 208550
HGET 284900 290951
HMSET (10 fields) 58937 111445
HMGET (10 fields) 203624 205592
HGETALL 166861 160797
ZADD 106780 189178
ZREM 112866 201938
PFADD 4708 4692
PFCOUNT 27412 27345
PFMERGE 478 494

1.4 Compare Wiht Redis

With Redis AOF configuration appendfsync everysec, redis basically write data into memeory. However, pika uses rocksdb, which writes WAL on ssd druing every write batch. That comparation becomes multiple threads sequential write into ssd and one thread write into memory.

Put the fairness or unfairness aside. Here is the performance.

2. Cluster (Codis)

2.1 Topology:

WithBInlog&Slave

4 machine * 2 pika instance (Master)

4 machine * 2 pika instance (Slave)

NoBinlog&Slave

4 machine * 2 pika instance (Master)

Slots Distribution:

2.2 WithBinlog&Slave

Command QPS
Set 1,400,000+

2.3 NoBinlog&Slave

Command QPS
Set 1,600,000+

2.4 Get Command

Command QPS
Get 2,300,000+

With or without binlog, for Get command, QPS is approximately the same.


File: docs/design/architecture.md

概述

Pika 一款开源的高性能持久化的NoSQL产品,兼容Redis协议,数据持久化存储到RocksDB存储引擎,具有两种运行模式: 经典模式(Classic) & 分布式模式(Sharding)。

  • 经典模式(Classic): 即1主N从同步模式,1个主实例存储所有的数据,N个从实例完全镜像同步主实例的数据,每个实例支持多个DBs。DB默认从0开始,Pika的配置项databases可以设置最大DB数量。DB在Pika上的物理存在形式是一个文件目录。
  • 分布式模式(Sharding): Sharding模式下,将用户存储的数据集合称为Table,每个Table切分成多个分片,每个分片称为Slot,对于某一个KEY的数据由哈希算法计算决定属于哪个Slot。将所有Slots及其副本按照一定策略分散到所有的Pika实例中,每个Pika实例有一部分主Slot和一部分从Slot。在Sharding模式下,分主从的是Slot而不再是Pika实例。Slot在Pika上的物理存在形式是一个文件目录。

Pika可以通过配置文件中的instance-mode配置项,设置为classic和sharding,来选择运行经典模式(Classic)还是分布式模式(Sharding)的Pika。

1. 经典(主从)模式

2. 分布式模式


File: docs/design/blackwidonw.md

Blackwidow本质上是基于rocksdb的封装,使本身只支持kv存储的rocksdb能够支持多种数据结构, 目前Blackwidow支持五种数据结构的存储:String结构(实际上就是存储key, value), Hash结构,List结构,Set结构和ZSet结构, 因为Rocksdb的存储方式只有kv一种, 所以上述五种数据结构最终都要落盘到Rocksdb的kv存储方式上,下面我们展示Blackwidow和rocksdb的关系并且说明我们是如何用kv来模拟多数据结构的。

1. String结构的存储

String本质上就是Key, Value, 我们知道Rocksdb本身就是支持kv存储的, 我们为了实现Redis中的expire功能,所以在value后面添加了4 Bytes用于存储timestamp, 作为最后Rocksdb落盘的kv格式,下面是具体的实现方式:

如果我们没有对该String对象设置超时时间,则timestamp存储的值就是默认值0, 否则就是该对象过期时间的时间戳, 每次我们获取一个String对象的时候, 首先会解析Value部分的后四字节, 获取到timestamp做出判断之后再返回结果。

2. Hash结构的存储

blackwidow中的hash表由两部分构成,元数据(meta_key, meta_value), 和普通数据(data_key, data_value), 元数据中存储的主要是hash表的一些信息, 比如说当前hash表的域的数量以及当前hash表的版本号和过期时间(用做秒删功能), 而普通数据主要就是指的同一个hash表中一一对应的field和value,作为具体最后Rocksdb落盘的kv格式,下面是具体的实现方式:

  1. 每个hash表的meta_key和meta_value的落盘方式:

meta_key实际上就是hash表的key, 而meta_value由三个部分构成: 4Bytes的Hash size(用于存储当前hash表的大小) + 4Bytes的Version(用于秒删功能) + 4Bytes的Timestamp(用于记录我们给这个Hash表设置的超时时间的时间戳, 默认为0)

  1. hash表中data_key和data_value的落盘方式:

data_key由四个部分构成: 4Bytes的Key size(用于记录后面追加的key的长度,便与解析) + key的内容 + 4Bytes的Version + Field的内容, 而data_value就是hash表某个field对应的value。

  1. 如果我们需要查找一个hash表中的某一个field对应的value, 我们首先会获取到meta_value解析出其中的timestamp判断这个hash表是否过期, 如果没有过期, 我们可以拿到其中的version, 然后我们使用key, version,和field拼出data_key, 进而找到对应的data_value(如果存在的话)

3. List结构的存储

blackwidow中的list由两部分构成,元数据(meta_key, meta_value), 和普通数据(data_key, data_value), 元数据中存储的主要是list链表的一些信息, 比如说当前list链表结点的的数量以及当前list链表的版本号和过期时间(用做秒删功能), 还有当前list链表的左右边界(由于nemo实现的链表结构被吐槽lrange效率低下,所以这次blackwidow我们底层用数组来模拟链表,这样lrange速度会大大提升,因为结点存储都是有序的), 普通数据实际上就是指的list中每一个结点中的数据,作为具体最后Rocksdb落盘的kv格式,下面是具体的实现方式

  1. 每个list链表的meta_key和meta_value的落盘方式:

meta_key实际上就是list链表的key, 而meta_value由五个部分构成: 8Bytes的List size(用于存储当前链表中总共有多少个结点) + 4Bytes的Version(用于秒删功能) + 4Bytes的Timestamp(用于记录我们给这个List链表设置的超时时间的时间戳, 默认为0) + 8Bytes的Left Index(数组的左边界) + 8Bytes的Right Index(数组的右边界)

  1. list链表中data_key和data_value的落盘方式:

data_key由四个部分构成: 4Bytes的Key size(用于记录后面追加的key的长度,便与解析) + key的内容 + 4Bytes的Version + 8Bytes的Index(这个记录的就是当前结点的在这个list链表中的索引), 而data_value就是list链表该node中存储的值

4. Set结构的存储

blackwidow中的set由两部分构成,元数据(meta_key, meta_value), 和普通数据(data_key, data_value), 元数据中存储的主要是set集合的一些信息, 比如说当前set集合member的数量以及当前set集合的版本号和过期时间(用做秒删功能), 普通数据实际上就是指的set集合中的member,作为具体最后Rocksdb落盘的kv格式,下面是具体的实现方式:

  1. 每个set集合的meta_key和meta_value的落盘方式:

meta_key实际上就是set集合的key, 而meta_value由三个部分构成: 4Bytes的Set size(用于存储当前Set集合的大小) + 4Bytes的Version(用于秒删功能) + 4Bytes的Timestamp(用于记录我们给这个set集合设置的超时时间的时间戳, 默认为0)

  1. set集合中data_key和data_value的落盘方式:

data_key由四个部分构成: 4Bytes的Key size(用于记录后面追加的key的长度,便与解析) + key的内容 + 4Bytes的Version + member的内容, 由于set集合只需要存储member, 所以data_value实际上就是空串

5. ZSet结构的存储

blackwidow中的zset由两部部分构成,元数据(meta_key, meta_value), 和普通数据(data_key, data_value), 元数据中存储的主要是zset集合的一些信息, 比如说当前zset集合member的数量以及当前zset集合的版本号和过期时间(用做秒删功能), 而普通数据就是指的zset中每个member以及对应的score, 由于zset这种数据结构比较特殊,需要按照memer进行排序,也需要按照score进行排序, 所以我们对于每一个zset我们会按照不同的格式存储两份普通数据, 在这里我们称为member to score和score to member,作为具体最后Rocksdb落盘的kv格式,下面是具体的实现方式:

  1. 每个zset集合的meta_key和meta_value的落盘方式:

meta_key实际上就是zset集合的key, 而meta_value由三个部分构成: 4Bytes的ZSet size(用于存储当前zSet集合的大小) + 4Bytes的Version(用于秒删功能) + 4Bytes的Timestamp(用于记录我们给这个Zset集合设置的超时时间的时间戳, 默认为0)

  1. 每个zset集合的data_key和data_value的落盘方式(member to score):

member to socre的data_key由四个部分构成:4Bytes的Key size(用于记录后面追加的key的长度,便与解析) + key的内容 + 4Bytes的Version + member的内容, data_value中存储的其member对应的score的值,大小为8个字节,由于rocksdb默认是按照字典序进行排列的,所以同一个zset中不同的member就是按照member的字典序来排列的(同一个zset的key size, key, 以及version,也就是前缀都是一致的,不同的只有末端的member).

  1. 每个zset集合的data_key和data_value的落盘方式(score to member):

score to member的data_key由五个部分构成:4Bytes的Key size(用于记录后面追加的key的长度,便与解析) + key的内容 + 4Bytes的Version + 8Bytes的Score + member的内容, 由于score和member都已经放在data_key中进行存储了所以data_value就是一个空串,无需存储其他内容了,对于score to member中的data_key我们自己实现了rocksdb的comparator,同一个zset中score to member的data_key会首先按照score来排序, 在score相同的情况下再按照member来排序

Blackwidow相对于Nemo有哪些优势

  1. Blackwidow采用了rocksdb的column families的新特性,将元数据和实际数据分开存放(对应于上面的meta数据和data数据), 这种存储方式相对于Nemo将meta, data混在一起存放更加合理, 并且可以提升查找效率(比如info keyspace的效率会大大提升)
  2. Blackwidow中参数传递大量采用Slice而Nemo中采用的是std::string, 所以Nemo会有很多没有必要的string对象的构造函数以及析构函数的调用,造成额外的资源消耗,而Blackwidow则不会有这个问题
  3. Blackwidow对kv模拟多数据结构的存储格式上做了重新设计(具体可以参考Nemo引擎数据存储格式和本篇文章),使之前在Nemo上出现的一些无法解决的性能问题得以解决,所以Blackwidow的多数据结构在某些场景下性能远远优于Nemo
  4. 原来Nemo对多数据结构的Key的长度最大只能支持到256 Bytes,而Blackwidow经过重新设计,放开了多数据结构Key长度的这个限制
  5. Blackwidow相对于Nemo更加节省空间,Nemo由于需要nemo-rocksdb的支持,所以不管在meta还是data数据部分都追加了version和timestamp这些信息,并且为了区分meta_key和data_key, 在最前面加入s和S(拿Set数据结构打比方),Blackwidow在这方面做了优化,使同样的数据量下Blackwidow所占用的空间比Nemo要小(举个例子,Blackwidow中List结构中的一个Node就比Nemo中的一个Node节省了16 Bytes的空间)
  6. Blackwidow在锁的实现上参照了RocksDB事务里锁的实现方法,而弃用了之前Nemo的行锁,所以在多线程对同一把锁有抢占的情况下性能会有所提升

File: docs/design/coding.md

c++ coding style

header file

Name and order of includes

顺序是

Related header, C library, C++ library, other libraries'.h, your project's .h`.

不要使用 . 以及 .. 这样的符号

比如一个项目的include 头文件应该是这样

c++
#include "foo/server/fooserver.h"

#include <sys/types.h>
#include 

#include <hash_map>
#include <vector>

#include "base/basictypes.h"
#include "base/commandlineflags.h"
#include "foo/server/bar.h"

class

Doing work in constructors

constructor 不能调用虚函数, 因为在构造的时候, 这个对象还没有完全生成, 因此调用虚函数肯定是不对的

Inheritance

使用 override 来表示某一个函数是virtual 函数的重新实现, 这样就不需要在看代码的时候确认这个函数是否是重载, 如果在子类里面对一个父类没有的函数进行override也是会直接报错的

在使用struct的时候 只用在只定义数据,不包含任何方法的结构体里面,出了简单的构造函数或者init函数.

通过使用继承可以有效的减少代码量 并且因为继承是编译时期的 因此在编译器能够识别这些错误 接口继承(也就是定义纯虚函数) 更是能够在编译期就识别一个继承的类是否实现了全部的接口

但是由于继承把一个类的代码分散在各个文件里面了 因此增加了看代码的难度 并且父类定义自己的成员变量 因此访问的时候不是很方便

所以一定要账号 is-a 和 has-a的关系 一定确定是 a是b的一种的时候才可以使用 继承 否则尽可能的使用组合 也就是b里面有一个a的成员变量

Function

parameter Ordering

函数的变量的顺序: input, 然后是output

尽量把一个函数控制在40行以内

reference Arguments

所有通过引用传参的变量都需要加上const, 也就是 const type &in

尽可能的input argument 用value 或者 const reference(当然如果这个变量就是指针, 那么传进来的时候就用指针), 然后output argument 用指针

还有就是如果变量需要传进来NULL的时候, 可能会用const T*

function overloading

尽可能的不要使用 function overloading, 因为function overloading 增加了c++ 的复杂性. 特别是当继承的时候, 子类只实现了父类的某一个function 的时候, 这样代码的复杂度就更麻烦了. 因为不知道重载的是哪一个函数, 因此

尽可能的不要使用function overloading, 当遇到函数需要不用的变量类型的时候, 可以写成这种AppendString(), AppendInt() 这种

default value

允许在非non-virtual 函数里面使用 default value

scoping

Nonmember, static member, global function

如果有一个函数和一个类的对象里面的内容并不相关

那么这个时候有两个选择, 可以定义成class static member function, nonmember function. 那么这个时候如何选择?

如果这个函数和这个对象强相关, 比如是建立一个这个对象, 或者操作这个类的静态成员函数的时候, 将这个函数声明成class static member function

否则将这个函数声明成nonmember function, 然后用namespace 隔离开来

如果有一个函数只在某一个.cc 文件里面使用, 那么可以将这个函数放在unnamed namespace 或者用static 声明 static int foo() 这种

other

关于exception 的使用

  • pros:
    • exception 可以发现更深层次的错误, 比如a()->b()->c()->d() 那么在d里面抛出的exception 在a里面是可以直接捕获的
    • 比如在c++ 的construction 里面, 我们是无法知道这个construction 是否构造成功,
  • cons
    • ​

关于返回值

  1. 在一个函数内部调用

brace initializer List

在c++11 里面可以直接通过{} 来初始化一个list, 这个是在c++ 11 之前都不可以的, 比如:

c++
int main()
{
  std::vector v{1, 2, 3};
  std::map mp{{1, 2}, {1, 3}, {1, 4}};
  return 0;
}

sizeof

在使用sizeof 的时候尽可能的去sizeof(varname), 而不是去sizeof(type). 因此这个varname 随时会更新, 如果varname 这个变量被赋值给其他对象的时候

注意sizeof 的时候考虑对齐的问题

Run-Time Type Information(RTTI)

c++ 允许在运行的过程中使用typeid, dynamic_cast 来检查一个变量的类型, 通过dynamic_cast 在类型转换的时候进行检查, 只允许父类的指针指向子类, 而不允许子类的指针指向父类

但是其实用RTTI 的代码都可以用其他的方式来写, 而RTTI 不是很高效, 因此尽可能用 virtual method, 或者 Visitor pattern 模式来实现比较好

cast

尽可能的使用 c++ 的static_cast, const_cast, reinterpret_cast 而不是用c 里面的cast

stream

如果你为了debug想要打印一个对象内部的细节, 那么经常会提供一个DebugString() 是最经常的

不要使用stream 作为外部用户的IO, stream 性能还是不行的

Friend

允许使用 Friend class, function

Friend class 虽然会破坏了类封装, 允许外部类直接访问当前这个类里面的私有成员, 常见的用法就是FooBuilder 应该能够访问Foo 里面的私有成员. 如果没有Friend class, 要么把Foo 的成员都设置成public, 要么给所有的成员变量都添加get, set 函数. 还是很不方便的.

Friend class 只是让某一个类可以访问这个类, 还是比让所有的成员变量都public 来说, 封装更好一些

因此Friend class 需要看到Foo 的私有变量, 因此经常将Friend class 放在同一个头文件里面

use of const

能用const 的地方尽可能的使用const

Integer type

用<stdint.h> 里面定义的int32_t, int64_t 等等, 而不适用short, long, long long 这种类型, 因为short, long 等是根据编译器和平台是不一样的

0 and nullptr/NULL

Use 0 for integers, 0.0 for reals, nullptr (or NULL) for pointers, and '\0' for chars.

在支持c++11 的项目里面尽可能使用nullptr

Comments

TODO comment

写TODO comment 的时候记得写上谁写的这个TODO

// TODO([email protected]): Use a "*" here for concatenation operator.

总结

最后可以用 cpplint.py 跑一下, 尽可能把错误是4, 以及4以上的给排除掉


File: docs/design/consistency.md

目前线程模型

非一致性场景

1,客户端请求加锁写入db和binlog文件

2,将结果返回客户端

3,通过发送BinlogSync请求向从库同步

4,从库返回BinlogSyncAck报告同步状况

一致性场景

1,客户端请求先写入binlog文件

2,通过发送BinlogSync请求向从库同步

3,从库返回BinlogSyncAck报告同步状况

4,将相应的请求写入db

5,将结果返回客户端

Binlog Header变动

text
/*
* *****************Type First Binlog Item Format******************
* |<Type>|<CreateTime>|<Term Id>|<Logic Id>|<File Num>|<Offset>|<Content Length>|<Content>|
* |  2   |      4     |    4    |     8    |    4     |   8    |       4        |   ...   |
* |----------------------------------------- 34 Bytes ------------------------------------|
*/

其中 TermId, 和Logic id是沿用Raft论文中term 和log index 的概念。具体的详见论文。

其中 File Num 和offset 是本条binlog在文件中的偏移量。

Pika的Binlog存在的意义是为了保证主从能够增量同步,而Raft Log存在的意义是为了保证Leader 和Follower 的数据够一致。某种意义上说这两个"Log"的概念是一样的,所以在实现上,将binlog 和 Raft Log 复用成一条log,目前的binlog header 中 Term Id 和 Logic Id 属于 Raft Log(简称日志)的信息,而File Num 和Offset 属于Binlog 的信息。

一致性协议的三阶段

日志恢复和复制基本按照Raft论文当中所做操作,这里不做过多解释。实现上,这里分为三个阶段。分别是日志的复制,日志的恢复,日志的协商。

熟悉Raft协议的读者可能会发现,这里的三个阶段跟Raft日志复制不是完全一样。在Pika的实现当中,基于pika现有的代码结构,我们把Leader 和Follower同步点位回退的逻辑单独提取出来,形成了Pika Trsysync的状态。任何日志的复制出错,pika会终止当前的日志复制(BinlogSync)状态,转而将状态机转化成Trysync的状态,进而Leader 和Follower 会进入日志的协商逻辑。协商成功之后会转入日志复制的逻辑。

日志复制

日志的逻辑结构如下,上面部分是Leader可能的log点位,下部分是Follower可能的log点位。

1,日志的复制的逻辑可以参考Raft协议的逻辑,这里举例说说明客户端从请求到返回,日志经过了怎样的流程。

Leader Status:

Committed Index : 10

Applied Index:8

Last Index: 15

Follower Status:

Committed Index : 7

Applied Index:5

LastIndex: 12

2,当Leader 发送13-15的日志到Follower的时候,Follower的状态会做如下更新:

Follower Status:

Committed Index : 10

Applied Index:5

LastIndex: 15

这时候日志6-10都是可以被应用到状态机的。但是对于日志11-15来说只能等到下一次收到Leader Committed Index 大于15的时候这些日志才能够被更新,这时候如果客户端没有继续写入,Follower的Committed index可以依靠ping消息(携带了Leader 的committed index)进行更新。

3,当Leader 接收到Follower的ack信息的时候,Leader 状态会做如下更新:

Leader Status:

Committed Index : 15

Applied Index: 8

Last Index: 15

此时日志9-15都是可以被应用到状态机,这里是写db,当日志9写入db之后,就会返回客户端,相应的Applied Index 更新为9。这时候日志9就可以返回客户端。

对于从来说,整体的日志复制的逻辑还是按照Raft论文当中进行的。唯一不同的是论文中日志回退的一部分逻辑放到了日志协商阶段进行。

日志恢复:

重启pika的时候,根据持久话的一致性信息(applied index 等)回复出之前的一致性状态。

日志协商:

这个阶段Follower节点主动发起Trysync流程,携带Last Index,向Leader发送协商同步点位请求。协商过程如下:

Follower携带last_index发动到Leader, Leader 通过Follower的last_index位置判断是否自己能够找到last_index对应的自己的相应的log,如果找到log并且两个log一致,Leader返回okay协商结束。如果没有找到,或者log不一致,Leader向Follower发送hints,hints是Leader本地的最新的日志。Follower通过hints,回退本地日志,更新自己的last_index,重新向主协商。最终Leader Follower达成一致,结束TrySync流程,进行日志复制流程。

Leader 日志协商的伪代码如下:

c++
Status LeaderNegotiate() {
  reject = true
  if (follower.last_index > last_index) {
    send[last_index - 100, last_index]
  } else if (follower.last_index < first_index) {
    need dbsync
  }
  if (follower.last_index not found) {
    need dbsync
  }
  if (follower.last_index found but term not equal) {
    send[found_index - 100, found_index]
  }
  reject = false
  return ok;
}

Follower日志协商的伪代码日下:

c++
Status FollowerNegotiate() {
  if last_index > hints[hints.size() - 1] {
    TruncateTo(hints[hints.size() - 1]);
  }
  for (reverse loop hints) {
    if (hint.index exist && hint.term == term) {
      TruncateTo(hint.index)
      send trysync with log_index = hint.index
      return ok;
    }
  }
  // cant find any match
  TruncateTo(hints[0])
  send trysync with log_index = last_index
}

以上介绍了关于日志的具体三个阶段。整体的逻辑遵从与Raft论文的设计,在实现细节上根据Pika目前的代码结构进行了一些列的调整。

关于选主和成员变换

目前选主需要管理员手动介入,详见副本一致性使用文档 。

成员变换的功能目前暂不支持。


File: docs/design/lock.md

pika作为类redis的存储系统,为了弥补在性能上的不足,在整个系统中大量使用多线程的结构,涉及到多线程编程,势必需要为线程加锁来保证数据访问的一致性和有效性。其中主要用到了三种锁

  1. 互斥锁
  2. 读写锁
  3. 行锁

读写锁

应用场景

应用挂起指令,在挂起指令的执行中,会添加写锁,以确保,此时没有其他指令执行。其他的普通指令在会添加读锁,可以并行访问。
其中挂起指令有:

  1. trysync
  2. bgsave
  3. flushall
  4. readonly

作用和意义

保证当前服务器在执行挂起指令时,起到阻写作用。

行锁

行锁,用于对一个key加锁,保证同一时间只有一个线程对一个key进行操作。

作用和意义:

pika中存取的数据都是类key,value数据,不同key所对应的数据完全独立,所以只需要对key加锁可以保证数据在并发访问时的一致性,行锁相对来说,锁定粒度小,也可以保证数据访问的高效性。

应用场景

在pika系统中,对于数据库的操作都需要添加行锁,主要在应用于两个地方,在系统上层指令过程中和在数据引擎层面。在pika系统中,对于写指令(会改变数据状态,如SET,HSET)需要除了更新数据库状态,还涉及到pika的增量同步,需要在binlog中添加所执行的写指令,用于保证master和slave的数据库状态一致。故一条写指令的执行,主要有两个部分:

  1. 更改数据库状态
  2. 将指令添加到binlog中

其加锁情况,如下图:

设计的平衡

在图中可以看到,对同一个key,加了两次行锁,在实际应用中,pika上所加的锁就已经能够保证数据访问的正确性。如果只是为了pika所需要的业务,blackwidow层面使用行锁是多余的,但是blackwidow的设计初衷就是通过对rocksdb的改造和封装提供一套完整的类redis数据访问的解决方案,而不仅仅是为pika提供数据库引擎。这种设计思路也是秉承了Unix中的设计原则:Write programs that do one thing and do it well。

这样设计大大降低了pika与blackwidow之间的耦合,也使得blackwidow可以被单独拿出来测试和使用,在pika中的数据迁移工具就是完全使用blackwidow来完成,不必依赖任何pika相关的东西。另外对于blackwidow感兴趣或者有需求的团队也可以直接将blackwidow作为数据库引擎而不需要修改任何代码就能使用完整的数据访问功能。

具体实现

在pika系统中,一把行锁就可以维护所有key。在行锁的实现上是将一个key与一把互斥锁相绑定,并将其放入哈希表中维护,来保证每次访问key的线程只有一个,但是不可能也不需要为每一个key保留一把互斥锁,只需要当有多条线程访问同一个key时才需要锁,在所有线程都访问结束之后,就可以销毁这个绑定key的互斥锁,释放资源。具体实现如下:

c++
class RecordLock {
 public:
  RecordLock(port::RecordMutex *mu, const std::string &key)
      : mu_(mu), key_(key) {
        mu_->Lock(key_);
      }
  ~RecordLock() { mu_->Unlock(key_); }

 private:
  port::RecordMutex *const mu_;
  std::string key_;

  // No copying allowed
  RecordLock(const RecordLock&);
  void operator=(const RecordLock&);
};

void RecordMutex::Lock(const std::string &key) {
  mutex_.Lock();
  std::unordered_map<std::string, RefMutex *>::const_iterator it = records_.find(key);

  if (it != records_.end()) {
    //log_info ("tid=(%u) >Lock key=(%s) exist, map_size=%u", pthread_self(), key.c_str(), records_.size());
    RefMutex *ref_mutex = it->second;
    ref_mutex->Ref();
    mutex_.Unlock();

    ref_mutex->Lock();
    //log_info ("tid=(%u) <Lock key=(%s) exist", pthread_self(), key.c_str());
  } else {
    //log_info ("tid=(%u) >Lock key=(%s) new, map_size=%u ++", pthread_self(), key.c_str(), records_.size());
    RefMutex *ref_mutex = new RefMutex();

    records_.insert(std::make_pair(key, ref_mutex));
    ref_mutex->Ref();
    mutex_.Unlock();

    ref_mutex->Lock();
    //log_info ("tid=(%u) <Lock key=(%s) new", pthread_self(), key.c_str());
  }
}

完整代码可参考:pstd_mutex.cc pstd_mutex.h


File: docs/design/nemo.md

nemo本质上是对rocksdb的改造和封装,使其支持多数据结构的存储(rocksdb只支持kv存储)。总的来说,nemo支持五种数据结构类型的存储:KV键值对(为了区分,nemo的的键值对结构用大写的“KV”表示)、Hash结构、List结构、Set结构和ZSet结构。因为rocksdb的存储方式只有kv一种结构,所以以上所说的5种数据结构的存储最终都要落盘到rocksdb的kv存储方式上。

1. KV键值对的存储。

KV存储没有添加额外的元信息,只是在value的结尾加上8个字节的附加信息(前4个字节表示version,后 4个字节表示ttl)作为最后落盘kv的值部分。具体如下图:

version字段用于对该键值对进行标记,以便后续的处理,如删除一个键值对时,可以在该version进行标记,后续再进行真正的删除,这样可以减少删除操作所导致的服务阻塞时间。

2. Hash结构的存储

对于每一个Hash存储,它包括hash键(key),hash键下的域名(field)和存储的值 (value)。nemo的存储方式是将key和field组合成为一个新的key,将这个新生成的key与所要存储的value组成最终落盘的kv键值对。同时,对于每一个hash键,nemo还为它添加了一个存储元信息的落盘kv,它保存的是对应hash键下的所有域值对的个数。下面的是具体的实现方式:

  1. 每个hash键、field、value到落盘kv的映射转换

  2. 每个hash键的元信息的落盘kv的存储格式

  3. 中前面的横条对应落盘kv的键部分,从前到后,第一个字段是一个字符’h’,表示的是hash结构的key;第二个字段是hash键的字符串长度,用一个字节(uint8_t类型)来表示;第三个字段是hash键的内容,因为第二个字段是一个字节,所以这里限定hash键的最大字符串长度是254个字节;第四个字段是field的内容。a中后面的横条代表的是落盘kv键值对的值部分,和KV结构存储一样,它是存入的value值加上8个字节的version字段和8个字节的ttl字段得到的。
    b中前面的横条代表的存储每个hash键的落盘kv键值对的键部分,它有两字段组成,第一个字段是一个’H’字符,表示这存储时hash键的元信息,第二个字段是对应的hash键的字符串内容;b中后面的横条代表的该元信息的值,它表示对应的hash键中的域值对(field-value)的数量,大小为8个字节(类型是int64_t)。

3. List结构的存储

顾名思义,每个List结构的底层存储也是采用链表结构来完成的。对于每个List键,它的每个元素都落盘为一个kv键值对,作为一个链表的一个节点,称为元素节点。和hash一样,每个List键也拥有自己的元信息。

a. 每个元素节点对应的落盘kv存储格式

b.每个元信息的落盘kv的存储格式

a中前面横条代表的是最终落盘kv结构的键部分,总共4个字段,前面三个字符段分别为一个字符’l’(表明是List结构的结存),List键的字符串长度(1个字节)、List键的字符串内容(最多254个字节),第四个字段是该元素节点所对应的索引值,用8个字节表示(int64_t类型),对于每个元素节点,这个索引(sequence)都是唯一的,是其他元素节点访问该元素节点的唯一媒介;往一个空的List键内添加一个元素节点时,该添加的元素节点的sequence为1,下次一次添加的元素节点的sequence为2,依次顺序递增,即使中间有元素被删除了,被删除的元素的sequence也不会被之后新插入的元素节点使用,这就保证了每个元素节点的sequence都是唯一的。b中后面的横条代表的是具体落盘kv结构的值,它有5个字段,后面的三个字段分别为存入的value值、version、ttl,这和前面的hash结构存储是类似的;前两个字段分别表示的是前一个元素节点的sequence、和后一个元素节点的sequence、通过这两个sequence,就可以知道前一个元素节点和后一个元素节点的罗盘kv的键内容,从而实现了一个双向链表的结构。

b中的前面横条表示存储元信息的落盘kv的键部分,和前面的hash结构是类似的;后面的横条表示存储List键的元信息,它有四个字段,从前到后分别为该List键内的元素个数、最左边的元素 节点的sequence(相当于链表头)、最右边的元素节点的sequence(相当于链表尾)、下一个要插入元素节点所应该使用的sequence。

4. Set结构的存储

a.每个元素节点对应的落盘kv存储格式

b.每个Set键的元信息对应的落盘kv存储格式

Set结构的存储和hash结构基本是相同的,只是Set中每个元素对应的落盘kv中,值的部分只有version和ttl,没有value字段。

5. ZSet结构的存储

ZSet存储结构是一个有序Set,所以对于每个元素,增加了一个落盘kv,在这个增加的落盘kv的键部分,把该元素对应的score值整合进去,这样便于依据score值进行排序(因为从rocksdb内拿出的数据时按键排序的),下面是落盘kv的存储形式。
a. score值在value部分的落盘kv存储格式

b. score值在key部分的落盘kv存储格式

c.存储元信息的落盘kv的存储格式

a、c与前面的几种数据结构类似,不再赘述。b中的score是从double类型转变过来的int64_t类型,这样做是为了可以让原来的浮点型的score直接参与到字符串的排序当中(浮点型的存储格式与字符串的比较方式不兼容)。


File: docs/design/snapshot.md

Pika 快照式备份方案

原理

不同于Redis,Pika的数据主要存储在磁盘中,这就使得其在做数据备份时有天然的优势,可以直接通过文件拷贝实现
实现

流程

  • 打快照:阻写(阻止客户端进行写db操作),并在这个过程中获取快照内容
  • 异步线程拷贝文件:通过修改Rocksdb提供的BackupEngine拷贝快照中文件,这个过程中会阻止文件的删除

快照内容

  • 当前db的所有文件名
  • manifest文件大小
  • sequence_number
  • 同步点
    • binlog filenum
    • offset

File: docs/design/sync.md

全量同步

背景

1.Pika Replicate

  • pika支持master/slave的复制方式,通过slave端的slaveof命令激发
  • salve端处理slaveof命令,将当前状态变为slave,改变连接状态
  • slave的向master发送MetaSync请求,在同步之前确保自身db的拓扑结构和master一致
  • slave下的每个partition单独的向master端对应的partition发起trysync请求,建立同步关系

2.Binlog

  • pika同步依赖binlog
  • binlog文件会自动或手动删除
  • 当同步点对应的binlog文件不存在时,需要通过全同步进行数据同步

全同步

1. 简介

  • 需要进行全同步时,master会将db文件dump后发送给slave
  • 通过rsync的deamon模式实现db文件的传输
  • 默认使用pika port+1000作为rysnc传输端口

2. 实现逻辑

  1. 在pika实例启动的同时会启动Rsync服务
  2. master发现某一个partition需要全同步时,判断是否有备份文件可用,如果没有先dump一份
  3. master通过rsync向slave发送对应partition的dump的文件
  4. slave的对应partition用收到的文件替换自己的db
  5. slave的对应partition用最新的偏移量再次发起trysnc
  6. 完成同步

Slave中某一个Partition建立同步:

Master处理同步请求:

3. Slave连接状态

  • No Connect:不尝试成为任何其他节点的slave
  • ShouldMetaSync:向master请求db的拓扑信息,确保与自身一致
  • TryConnect:为每个partition重置状态机,让其处于准备同步的状态
  • Connecting:在所有partition没有建立同步关系之前一直是处于connecting的状态
  • EstablishSucces: 所有partition建立同步关系成功
  • Error:出现了异常

增量同步

背景 :

从库Pika得到主库的全部DB结构,接下来以partition维度做Trysync,如果从库确认可以增量同步,从库将以partition为维度进行增量同步。默认使用pika port+2000的端口进行增量同步。

Binlog 结构:

Pika的主从同步是使用Binlog来完成的,一主多从的结构master节点也可以给多个slave复用一个Binlog,只不过不同的slave在binglog中有自己的偏移量而已,master执行完一条写命令就将命令追加到Binlog中,pika的同步模块会读出对应的binlog发送到slave,而slave收到binlog后会执行并追加到自己的Binlog中,由于主从偏移量一样,所以一旦发生网络或节点故障需要重连主从时,slave仅需要将自己当前的Binlog偏移量发送给master,master找到后从该偏移量开始同步后续命令,理论上将命令不做处理一条一条追加到文件中,但是这样的记录格式容错很差,如果读文件中写错一个字节则导致整个文件不可用,所以pika采用了类似leveldb log的格式来进行存储,具体如下:

交互过程:

1,从库发送BinlogSyncRequest 报文,报文中需说明自己已经收到的BinlogOffset。

2,主库收到BinlogSyncRequest之后会从同步点开始发出一批BinlogSyncResponse。

3,从库在收到BinlogSyncResponse之后,会在写入本地binlog之后再进行1流程。

同步模块:

Pika的同步由ReplicaManager(RM)模块负责。RM中有两层结构,逻辑层负责同步逻辑,传输层负责链接的管理数据的解析和传输。

数据的同步的最小单位是Partition,每一个Pika实例会维护自己作为主的partition(MasterPartition)和自己是从的partition(SlavePartition)。对于MasterPartition,需要记录跟随自己的slave同步信息,逻辑层会根据该信息向slave同步信息。对于SlavePartition,则是需要需要记录master的信息,逻辑层会按照该信息按需向master发送同步请求。

逻辑层维护两个数据结构,一个是MasterPartitions,记录跟随自己的SlaveNode信息,主要包括slave的同步状态和当前的sessionId。另一个是SlavePartitions,记录主的信息。

传输层分为两个子模块,ReplicationClient负责发起链接的建立,ReplicationServer负责响应报文。每两个实例之间的所有partition复用一条链接。

同步过程:

MasterPartition 同步事件

逻辑层处理MasterPartition的同步事件,向其对应的从同步数据。

1,读取MasterPartition Binlog信息后,将BinlogOffsetInfo记录到SlaveNode自己的window中。

2,将Binlog暂存到临时的待发送队列中。

3,辅助线程(Auxiliary thread) 定时将临时待发送队列的数据通过RM的传输层发送给对应的slave节点。

4,收到slave的BinlogSyncResponse之后,得知slave收到的BinlogOffset信息,更新SlaveNode window,重复1流程继续同步

为了控制每个SlaveNode同步的速度,避免某几个SlaveNode占用过多资源,为每一个SlaveNode设置了window。如下图所示,Pika收到了BinlogOffset为100到200的ack response,从window中移除BinlogOffset位于100到200的元素,之后继续发送BinlogOffset为1100和1200的binlog,对应的BinlogOffset添加至window中。

SlavePartition 同步事件

逻辑层处理SlavePartition的同步事件,收到master发送的同步数据,向master发相应的response信息。

1,按照解析出的Partition信息,被分配到对应的线程处理binlog写入任务。

2,线程写入binlog之后,调用传输层发送BinlogSyncResponse信息。

3,根据binlog的key分配给对应的线程处理写入db任务。