Flink CDC 原理、實踐和優化

CDC 變更數據捕獲技術可以將源數據庫的增量變動記錄,同步到一個或多個數據目的。本文基于騰訊云 Oceanus 提供的 Flink CDC 引擎,著重介紹 Flink 在變更數據捕獲技術中的應用。

一、CDC 是什么?

CDC 是變更數據捕獲(Change Data Capture)技術的縮寫,它可以將源數據庫(Source)的增量變動記錄,同步到一個或多個數據目的(Sink)。在同步過程中,還可以對數據進行一定的處理,例如分組(GROUP BY)、多表的關聯(JOIN)等。

例如對于電商平臺,用戶的訂單會實時寫入到某個源數據庫;A 部門需要將每分鐘的實時數據簡單聚合處理后保存到 Redis 中以供查詢,B 部門需要將當天的數據暫存到 Elasticsearch 一份來做報表展示,C 部門也需要一份數據到 ClickHouse 做實時數倉。隨著時間的推移,后續 D 部門、E 部門也會有數據分析的需求,這種場景下,傳統的拷貝分發多個副本方法很不靈活,而 CDC 可以實現一份變動記錄,實時處理并投遞到多個目的地。

下圖是一個示例,通過騰訊云 Oceanus 提供的 Flink CDC 引擎,可以將某個 MySQL 的數據庫表的變動記錄,實時同步到下游的 Redis、Elasticsearch、ClickHouse 等多個接收端。這樣大家可以各自分析自己的數據集,互不影響,同時又和上游數據保持實時的同步。

二、CDC 的實現原理

通常來講,CDC 分為主動查詢事件接收兩種技術實現模式。

對于主動查詢而言,用戶通常會在數據源表的某個字段中,保存上次更新的時間戳或版本號等信息,然后下游通過不斷的查詢和與上次的記錄做對比,來確定數據是否有變動,是否需要同步。這種方式優點是不涉及數據庫底層特性,實現比較通用;缺點是要對業務表做改造,且實時性不高,不能確保跟蹤到所有的變更記錄,且持續的頻繁查詢對數據庫的壓力較大。

事件接收模式可以通過觸發器(Trigger)或者日志(例如 Transaction log、Binary log、Write-ahead log 等)來實現。當數據源表發生變動時,會通過附加在表上的觸發器或者 binlog 等途徑,將操作記錄下來。下游可以通過數據庫底層的協議,訂閱并消費這些事件,然后對數據庫變動記錄做重放,從而實現同步。這種方式的優點是實時性高,可以精確捕捉上游的各種變動;缺點是部署數據庫的事件接收和解析器(例如 Debezium、Canal 等),有一定的學習和運維成本,對一些冷門的數據庫支持不夠。

綜合來看,事件接收模式整體在實時性、吞吐量方面占優,如果數據源是 MySQL、PostgreSQL、MongoDB 等常見的數據庫實現,建議使用 Debezium(https://debezium.io/documentation/reference/1.4/connectors/index.html)來實現變更數據的捕獲(下圖來自 Debezium 官方文檔 [https://debezium.io/documentation/reference/architecture.html])。如果使用的只有 MySQL,則還可以用 Canal (https://github.com/alibaba/canal)。

三、為什么選 Flink?

從上圖可以看到,Debezium 官方架構圖中,是通過 Kafka Streams 直接實現的 CDC 功能。而我們這里更建議使用 Flink CDC 模塊,因為 Flink 相對 Kafka Streams 而言,有如下優勢:

  • Flink 的算子和 SQL 模塊更為成熟和易用
  • Flink 作業可以通過調整算子并行度的方式,輕松擴展處理能力
  • Flink 支持高級的狀態后端(State Backends),允許存取海量的狀態數據
  • Flink 提供更多的 Source 和 Sink 等生態支持
  • Flink 有更大的用戶基數和活躍的支持社群,問題更容易解決
  • Flink 的開源協議允許云廠商進行全托管的深度定制,而 Kafka Streams 只能自行部署和運維

而且 Flink Table / SQL 模塊將數據庫表和變動記錄流(例如 CDC 的數據流)看做是同一事物的兩面(https://ci.apache.org/projects/flink/flink-docs-release-1.12/zh/dev/table/streaming/dynamic_tables.html),因此內部提供的 Upsert 消息結構(+I 表示新增、-U 表示記錄更新前的值、+U 表示記錄更新后的值,-D 表示刪除)可以與 Debezium 等生成的變動記錄一一對應。

四、Flink CDC 的使用方法

目前 Flink CDC 支持兩種數據源輸入方式。

(一)輸入 Debezium 等數據流進行同步

例如 MySQL -> Debezium -> Kafka -> Flink -> PostgreSQL。適用于已經部署好了 Debezium,希望暫存一部分數據到 Kafka 中以供多次消費,只需要 Flink 解析并分發到下游的場景。

image.png

在該場景下,由于 CDC 變更記錄會暫存到 Kafka 一段時間,因此可以在這期間任意啟動/重啟 Flink 作業進行消費;也可以部署多個 Flink 作業對這些數據同時處理并寫到不同的數據目的(Sink)庫表中,實現了 Source 變動與 Sink 的解耦。

用法示例

例如我們有個 MySQL 數據庫,需要實時將內容同步到 PostgreSQL 中。假設已經安裝部署好 Debezium 并開始消費 PostgreSQL 的變更日志,這些日志在持續寫入名為 YourDebeziumTopic 的 Kafka 主題中。我們可以新建一個 Flink SQL 作業,然后輸入如下 SQL 代碼(連接參數都是虛擬的,僅供參考):

CREATE TABLE `Data_Input` (
    id BIGINT,
    actor VARCHAR,
    alias VARCHAR,
    PRIMARY KEY (`id`) NOT ENFORCED
) WITH (
    'connector' = 'kafka',                             -- 可選 'kafka','kafka-0.11'. 注意選擇對應的內置  Connector
    'topic' = 'YourDebeziumTopic',                     -- 替換為您要消費的 Topic
    'scan.startup.mode' = 'earliest-offset'            -- 可以是 latest-offset / earliest-offset / specific-offsets / group-offsets 的任何一種
    'properties.bootstrap.servers' = '10.0.1.2:9092',  -- 替換為您的 Kafka 連接地址
    'properties.group.id' = 'YourGroup',               -- 必選參數, 一定要指定 Group ID

    -- 定義數據格式 (Debezium JSON 格式)
    'format' = 'debezium-json',
    'debezium-json.schema-include' = 'false',
);

CREATE TABLE `Data_Output` (
    id BIGINT,
    actor VARCHAR,
    alias VARCHAR,
    PRIMARY KEY (`id`) NOT ENFORCED
) WITH (
    'connector' = 'jdbc',
    'url' = 'jdbc:postgresql://postgresql.example:50060/myDatabase?currentSchema=mySchema&reWriteBatchedInserts=true', -- 請替換為您的實際 PostgreSQL 連接參數
    'table-name' = 'MyTable',   -- 需要寫入的數據表
    'username' = 'user',        -- 數據庫訪問的用戶名(需要提供 INSERT 權限)
    'password' = 'helloworld'   -- 數據庫訪問的密碼
);

INSERT INTO `Data_Output` SELECT * FROM `Data_Input`;

如果在流計算 Oceanus 界面上,可以勾選 kafkajdbc 兩個內置的 Connector:

image.png

隨后直接開始運行作業,Flink 就會源源不斷的消費 YourDebeziumTopic 這個 Kafka 主題中 Debezium 寫入的記錄,然后輸出到下游的 MySQL 數據庫中,實現了數據同步。

(二)直接對接上游數據庫進行同步

我們還可以跳過 Debezium 和 Kafka 的中轉,使用 Flink CDC Connectors(https://github.com/ververica/flink-cdc-connectors)對上游數據源的變動進行直接的訂閱處理。從內部實現上講,Flink CDC Connectors 內置了一套 Debezium 和 Kafka 組件,但這個細節對用戶屏蔽,因此用戶看到的數據鏈路如下圖所示:

image.png

用法示例

同樣的,這次我們有個 MySQL 數據庫,需要實時將內容同步到 PostgreSQL 中。但我們沒有也不想安裝 Debezium 等額外組件,那我們可以新建一個 Flink SQL 作業,然后輸入如下 SQL 代碼(連接參數都是虛擬的,僅供參考):

![image.png](https://upload-images.jianshu.io/upload_images/27263473-04e7f101b01ae861.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

如果在流計算頁面,可以選擇內置的 mysql-cdcjdbc Connector:

image.png

注意需要使用 Flink CDC Connectors(https://github.com/ververica/flink-cdc-connectors)附加組件。騰訊云 Oceanus 已經自帶了 MySQL-CDC Connector,如果自行部署的話,需要下載 jar 包并將其放入 Flink 的 lib 目錄下。訪問數據庫時,請確保連接的用戶足夠權限(PostgreSQL 用戶看這里[https://debezium.io/documentation/reference/connectors/postgresql.html#postgresql-permissions],MySQL 用戶看這里[https://debezium.io/documentation/reference/connectors/mysql.html#setting-up-mysql])。

五、Flink CDC 模塊的實現

(一)Debezium JSON 格式解析類探秘

flink-json 模塊中的 org.apache.flink.formats.json.debezium.DebeziumJsonFormatFactory 是負責構造解析 Debezium JSON 格式的工廠類;同樣地,org.apache.flink.formats.json.canal.CanalJsonFormatFactory 負責 Canal JSON 格式。這些類已經內置在 Flink 1.11 的發行版中,直接可以使用,無需附加任何程序包。對于 Debezium JSON 格式而言,Flink 將具體的解析邏輯放在了 org.apache.flink.formats.json.debezium.DebeziumJsonDeserializationSchema#DebeziumJsonDeserializationSchema 類中。

image.png

上圖表示 Debezium JSON 的一條更新(Update)消息,它表示上游已將 id=123 的數據更新,且字段內包含了更新前的舊值,以及更新后的新值。

那么,Flink 是如何解析并生成對應的 Flink 消息呢?我們看下這個類的 deserialize 方法:

GenericRowData before = (GenericRowData) payload.getField(0);  // 更新前的數據
GenericRowData after = (GenericRowData) payload.getField(1);  // 更新后的數據
String op = payload.getField(2).toString();            // 獲取 "op" 字段的類型
if (OP_CREATE.equals(op) || OP_READ.equals(op)) {  // 如果是創建 (c) 或快照讀取 (r) 消息    
after.setRowKind(RowKind.INSERT);        // 設置消息類型為新建 (+I)    
out.collect(after);                // 發送給下游
} else if (OP_UPDATE.equals(op)) {        // 如果是更新 (u) 消息    
before.setRowKind(RowKind.UPDATE_BEFORE);  // 把更新前的數據類型設置為撤回 (-U)    
after.setRowKind(RowKind.UPDATE_AFTER);    // 把更新后的數據類型設置為更新 (+U)    
out.collect(before);            // 發送兩條數據給下游    
out.collect(after);
} else if (OP_DELETE.equals(op)) {    // 如果是刪除 (d) 消息    
before.setRowKind(RowKind.DELETE);  // 將消息類型設置為刪除 (-D)    out.collect(before);        // 發送給下游
} else {    
...  // 異常處理邏輯
}

從上述邏輯可以看出,對于每一種 Debezium 的操作碼(op 字段的類型),都可以用 Flink 的 RowKind 類型來表示。對于插入 +I 和刪除 D,都只需要一條消息即可;而對于更新,則涉及刪除舊數據和寫入新數據,因此需要 -U+U 兩條消息來對應。

特別地,在 MySQL、PostgreSQL 等支持 Upsert(原子操作的 Update or Insert)語義的數據庫中,通常前一個 -U 消息可以省略,只把后一個 +U 消息用作實際的更新操作即可,這個優化在 Flink 中也有實現。

因此可以看到,Debezium 到 Flink 消息的轉換邏輯是非常簡單和自然的,這也多虧了 Flink 先進的設計理念,很早就提出并實現了 Upsert 數據流和動態數據表之間的映射關系。

1.Flink CDC Connectors 的實現

(1)flink-connector-debezium 模塊

我們在使用 Flink CDC Connectors 時,也會好奇它究竟是如何做到的不需要安裝和部署外部服務就可以實現 CDC 的。當我們閱讀 flink-connector-mysql-cdc 的源碼時,可以看到它內部依賴了 flink-connector-debezium 模塊,而這個模塊將 Debezium Embedded(https://github.com/debezium/debezium/tree/master/debezium-embedded)嵌入到了 Connector 中。

flink-connector-debezium 的數據源實現類為 com.alibaba.ververica.cdc.debezium.DebeziumSourceFunction,它集成了 Flink 中的 RichSourceFunction 并實現了 CheckpointedFunction 以支持快照保存狀態。

通常而言,對于 SourceFunction,我們可以從它的 run 方法入手分析。它的核心代碼如下:

this.engine = DebeziumEngine.create(Connect.class)    
      .using(properties) // 初始化 Debezium 所需的參數    
      .notifying(debeziumConsumer) // 收到批量的變更消息, 則 Debezium 會回調 DebeziumChangeConsumer 來反序列化并向下游輸出數據      
      .using(OffsetCommitPolicy.always())    
      .using(            
              (success, message, error) -> {                
                if (!success && error != null) {                    
                  this.reportError(error);                
                }            
              })    
      .build();
...     
      executor.execute(engine);  // 向 Executor 提交 Debezium 線程以啟動運行

可以看到,這個 SourceFunction 使用一些預先定義的參數,初始化了一個嵌入式的 DebeziumEngine(Java 的 Runnable),然后提交給線程池(executor)去執行。這個 Debezium 線程會批量接收 binlog 信息并回調傳入的 debeziumConsumer 以反序列化消息并交給 Flink 來處理。本類的其他方法主要負責初始化狀態和保存快照,這里略過。

這里我們再來看一下 DebeziumChangeConsumer 的實現,它的最核心的方法是 handleBatch 。當 Debezium 收到一批新的事件時,會調用這個方法來通知我們的 Connector 進行處理。這里有個 for 循環輪詢的邏輯:

for (ChangeEvent<SourceRecord, SourceRecord> event : changeEvents) {  // 輪詢各個事件
    SourceRecord record = event.value();
    if (isHeartbeatEvent(record)) {    // 如果時心跳包
        // 只更新當前 offset 信息, 然后繼續(不進行實際處理)
        synchronized (checkpointLock) {
            debeziumOffset.setSourcePartition(record.sourcePartition());
            debeziumOffset.setSourceOffset(record.sourceOffset());
        }
        continue;
    }

    deserialization.deserialize(record, debeziumCollector);    // 反序列化這條消息

    if (isInDbSnapshotPhase) {  // 如果處于數據庫快照期, 需要阻止 Flink 檢查點(Checkpoint)生成
        if (!lockHold) {
            MemoryUtils.UNSAFE.monitorEnter(checkpointLock);
            lockHold = true;
            ...
        }
        if (!isSnapshotRecord(record)) {  // 如果已經不在數據庫快照期了, 就釋放鎖, 允許 Flink 正常生成檢查點(Checkpoint)
            MemoryUtils.UNSAFE.monitorExit(checkpointLock);
            isInDbSnapshotPhase = false;
            ...
        }
    }

  // 更新當前 offset 信息, 并向下游 Flink 算子發送數據
    emitRecordsUnderCheckpointLock(
        debeziumCollector.records, record.sourcePartition(), record.sourceOffset());
}

可以看到邏輯比較簡單,只需要關注 checkpointLock 這個對象:只有持有這個對象的鎖時,才允許 Flink 進行檢查點的生成。

當作業處于數據庫快照期(即作業剛啟動時,需全量同步源數據庫的一份完整快照,此時收到的數據類型是 Debezium 的 SnapshotRecord),則不允許 Flink 進行 Checkpoint 即檢查點的生成,以避免作業崩潰恢復后狀態不一致;同樣地,如果正在向下游算子發送數據并更新 offset 信息時,也不允許快照的進行。這些操作都是為了保證 Exacly-Once(精確一致)語義。

這里也解釋了在作業剛啟動時,如果數據庫較大(同步時間較久),Flink 剛開始的 Checkpoint 永遠失敗(超時)的原因:只有當 Flink 完整同步了全量數據后,才可以進行增量數據的處理,以及 Checkpoint 的生成。

(2)flink-connector-mysql-cdc 模塊

而對于 flink-connector-mysql-cdc 模塊而言,它主要涉及到 MySQLTableSource 的聲明和實現。

我們知道,Flink 是通過 Java 的 SPI(Service Provider Interface)機制動態加載 Connector 的,因此我們首先看這個模塊的 src/main/resources/META-INF/services/org.apache.flink.table.factories.Factory 文件,里面內容指向 com.alibaba.ververica.cdc.connectors.mysql.table.MySQLTableSourceFactory

打開這個工廠類,我們可以看到它定義了該 Connector 所需的參數,例如 MySQL 數據庫的用戶名、密碼、表名等信息,并負責 MySQLTableSource 實例的具體創建,而 MySQLTableSource 類對這些參數做轉換,最終會生成一個上文提到的 DebeziumSourceFunction 對象。

因此我們可以發現,這個模塊作用是一個 MySQL 參數的封裝和轉換層,最終的邏輯實現仍然是由 flink-connector-debezium 完成的。

六、MySQL CDC 常見問題&優化

由于 Flink 的 CDC 功能還比較新(1.11 版本剛開始支持,1.12 版本逐步完善),因而在應用過程中,很可能會遇到有各種問題。鑒于大多數客戶的數據源都是 MySQL,我們這里整理了客戶常見的一些問題和優化方案,希望能夠幫助到大家。

Debezium 報錯:binlog probably contains events generated with statement or mixed based replication format

當前的 Binlog 格式被設置為了 STATEMENT 或者 MIXED, 這兩種都不被 Debezium 支持。為了使用 Flink CDC 功能,需要把 MySQL 的 binlog-format 設置為 ROW

SET GLOBAL binlog_format = 'ROW';
SET GLOBAL binlog_row_image = 'FULL';

如果您使用的是騰訊云的 TencentDB for MySQL,請確認下面設置:

image.png

Debezium 報錯:User does not have the 'LOCK TABLES' privilege required to obtain a consistent snapshot 或 Access denied; you need (at least one of) the SUPER, REPLICATION CLIENT privilege(s)

請對作業中指定的 MySQL 用戶賦予如下權限:SELECT, RELOAD, SHOW DATABASES, REPLICATION SLAVE, REPLICATION CLIENT,例如:

GRANT SELECT, RELOAD, SHOW DATABASES, REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO '用戶名' IDENTIFIED BY '密碼';
FLUSH PRIVILEGES;

如果您使用的數據庫不允許或者不希望使用 RELOAD 進行全局鎖,則還需要授予 LOCK TABLES 權限以令 Debezium 嘗試進行表級鎖。注意,表級鎖會導致更長的數據庫鎖定時間!
如果希望徹底跳過鎖(對數據的一致性要求不高,但要求數據庫不能被鎖),則可以在 WITH 參數中設置 'debezium.snapshot.locking.mode' = 'none' 參數來跳過鎖操作。但請注意,同步過程中千萬不要隨意變更庫表的結構。

作業剛啟動期間,Flink Checkpoint 一直失敗/重啟

前文講過,Flink CDC Connector 在初始的全量快照同步階段,會屏蔽掉快照的執行,因此如果 Flink Checkpoint 需要執行的話,就會因為一直無法獲得 checkpointLock 對象的鎖而超時。

可以設置 Flink 的 execution.checkpointing.tolerable-failed-checkpoint 參數以容忍更多的 Checkpoint 失敗事件,同時可以調大 Checkpoint 周期,避免作業因 Checkpoint 失敗而一直重啟。

JDBC Sink 批量寫入時,數據會缺失幾條

如果發現數據庫中的某些數據在 CDC 同步后有缺失,請確認是否仍在使用 Flink 舊版 1.10 的 Flink SQL WITH 語法(例如 WITH 參數中的 connector.type 是舊語法[https://ci.apache.org/projects/flink/flink-docs-release-1.10/dev/table/connect.html#jdbc-connector]connector 是新語法[https://ci.apache.org/projects/flink/flink-docs-stable/dev/table/connectors/jdbc.html#how-to-create-a-jdbc-table])。

舊版語法的 Connector 在 JDBC 批量寫入 Upsert 數據(例如數據庫的更新記錄)時,并未考慮到 Upsert 與 Delete 消息之間的順序關系,因此會出現錯亂的問題,請盡快遷移到新版的 Flink SQL 語法。

異常數據造成作業持續重啟

默認情況下,如果遇到異常的數據(例如消費的 Kafka topic 在無意間混入了其他數據),Flink 會立刻崩潰重啟,然后從上個快照點(Checkpoint)重新消費。由于某條異常數據的存在,作業會永遠因為異常而重啟。可以在 WITH 參數中加入 'debezium-json.ignore-parse-errors' = 'true' 來應對這個問題。

上游 Debezium 崩潰導致寫入重復數據,結果不準

Debezium 服務端發生異常并恢復后,由于可能沒有及時記錄崩潰前的現場,可能會退化為 At least once 模式,即同樣的數據可能被發送多次,造成下游結果不準確。

為了應對這個問題,新版的 Flink 1.12 增加了一個 table.exec.source.cdc-events-duplicate 配置項(可以編輯 flink-conf.yaml 文件來配置),建議將其設置為 true 以對這些重復數據進行去重。

但是需要注意,該選項需要數據源表定義了主鍵,否則也無法進行去重操作。

七、未來展望

在 Flink 1.11 版本中,CDC 功能首次被集成到內核中。由于 Flink 1.11.0 版本有個 嚴重 Bug(https://issues.apache.org/jira/browse/FLINK-18461)造成 Upsert 數據無法寫入下游,我們建議使用 1.11.1 及以上版本。

在 1.12 版本上,Flink 還在配置項中增加了前文提到的 table.exec.source.cdc-events-duplicate 等選項以更好地支持 CDC 去重;還支持 Avro 格式的 Debezium 數據流,而不僅僅限于 JSON 了。另外,這個版本增加了對 Maxwell(https://maxwells-daemon.io/)格式的 CDC 數據流支持,

為了更好地完善 CDC 功能模塊,Flink 社區創建了 [FLINK-18822] 以追蹤關于該模塊的進展。可以從中看到,Flink 1.13 主要著力于支持更多的類型(FLINK-18758[https://issues.apache.org/jira/browse/FLINK-18758]),以及允許從 Debezium Avro、Canal 等數據流中讀取一些元數據信息等。

而在更遠的規劃中,Flink 還可能支持基于 CDC 的內存數據庫緩存,這樣我們可以在內存中動態地 JOIN 一個數據庫的副本,而不必每次都查詢源庫,這將極大地提升作業的處理能力,并降低數據庫的查詢壓力。

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,156評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,401評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,069評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,873評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,635評論 6 408
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,128評論 1 323
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,203評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,365評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,881評論 1 334
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,733評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,935評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,475評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,172評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,582評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,821評論 1 282
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,595評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,908評論 2 372

推薦閱讀更多精彩內容