MySQL到ClickHouse的高速公路-MaterializeMySQL引擎(ClickHouse MaterializeMySQL)
引言

熟悉Mysql的朋友應該都知道,Mysql集群主從間數據同步機制十分完善。令人驚喜的是,ClickHouse作為近年來炙手可熱的大數據分析引擎也可以掛載為MySQL的從庫,作為MySQL的 "協處理器" 面向OLAP場景提供高效數據分析能力。早先的方案比較直截了當,通過第三方插件將所有MySQL上執行的操作進行轉化,然后在ClickHouse端逐一回放達到數據同步。終于在2020年下半年,Yandex 公司在 ClickHouse 社區發布了MaterializeMySQL引擎,支持從MySQL全量及增量實時數據同步。MaterializeMySQL引擎目前支持 MySQL 5.6/5.7/8.0 版本,兼容 Delete/Update 語句,及大部分常用的 DDL 操作。
基礎概念
MySQL & ClickHouse
MySQL一般特指完整的MySQL RDBMS,是開源的關系型數據庫管理系統,目前屬于Oracle公司。MySQL憑借不斷完善的功能以及活躍的開源社區,吸引了越來越多的企業和個人用戶。
ClickHouse是由Yandex公司開源的面向OLAP場景的分布式列式數據庫。ClickHouse具有實時查詢,完整的DBMS及高效數據壓縮,支持批量更新及高可用。此外,ClickHouse還較好地兼容SQL語法并擁有開箱即用等諸多優點。
Row Store & Column Store
MySQL存儲采用的是Row Store,表中數據按照 Row 為邏輯存儲單元在存儲介質中連續存儲。這種存儲方式適合隨機的增刪改查操作,對于按行查詢較為友好。但如果選擇查詢的目標只涉及一行中少數幾個屬性,Row 存儲方式也不得不將所有行全部遍歷再篩選出目標屬性,當表屬性較多時查詢效率通常較低。盡管索引以及緩存等優化方案在 OLTP 場景中能夠提升一定的效率,但在面對海量數據背景的 OLAP 場景就顯得有些力不從心了。
ClickHouse 則采用的是 Column Store,表中數據按照Column為邏輯存儲單元在存儲介質中連續存儲。這種存儲方式適合采用 SIMD (Single Instruction Multiple Data) 并發處理數據,尤其在表屬性較多時查詢效率明顯提升。列存方式中物理相鄰的數據類型通常相同,因此天然適合數據壓縮從而達到極致的數據壓縮比。
使用方法
部署Master-MySQL
開啟BinLog功能:ROW模式
開啟GTID模式:解決位點同步時MySQL主從切換問題(BinLog reset導致位點失效)
# my.cnf關鍵配置 gtid_mode=ON enforce_gtid_consistency=1 binlog_format=ROW
部署Slave-ClickHouse
獲取 ClickHouse/Master 代碼編譯安裝
推薦使用GCC-10.2.0,CMake 3.15,ninja1.9.0及以上
創建Master-MySQL中database及table
creat databases master_db; use master_db; CREATE TABLE IF NOT EXISTS `runoob_tbl`( `runoob_id` INT UNSIGNED AUTO_INCREMENT, `runoob_` VARCHAR(100) NOT NULL, `runoob_author` VARCHAR(40) NOT NULL, `submission_date` DATE, PRIMARY KEY ( `runoob_id` ) )ENGINE=InnoDB DEFAULT CHARSET=utf8; # 插入幾條數據 INSERT INTO runoob_tbl (runoob_, runoob_author, submission_date) VALUES ("MySQL-learning", "Bob", NOW()); INSERT INTO runoob_tbl (runoob_, runoob_author, submission_date) VALUES ("MySQL-learning", "Tim", NOW());
創建 Slave-ClickHouse 中 MaterializeMySQL database
# 開啟materialize同步功能 SET allow_experimental_database_materialize_mysql=1; # 創建slave庫,參數分別是("mysqld服務地址", "待同步庫名", "授權賬戶", "密碼") CREATE DATABASE slave_db ENGINE = MaterializeMySQL('192.168.6.39:3306', 'master_db', 'root', '3306123456');
DESKTOP:) select * from runoob_tbl; SELECT * FROM runoob_tbl Query id: 6e2b5f3b-0910-4d29-9192-1b985484d7e3 ┌─runoob_id─┬─runoob_title───┬─runoob_author─┬─submission_date─┐ │ 1 │ MySQL-learning │ Bob │ 2021-01-06 │ └───────────┴────────────────┴───────────────┴─────────────────┘ ┌─runoob_id─┬─runoob_title───┬─runoob_author─┬─submission_date─┐ │ 2 │ MySQL-learning │ Tim │ 2021-01-06 │ └───────────┴────────────────┴───────────────┴─────────────────┘ 2 rows in set. Elapsed: 0.056 sec.
工作原理
BinLog Event
MySQL中BinLog Event主要包含以下幾類:
1. MYSQL_QUERY_EVENT -- DDL
2. MYSQL_WRITE_ROWS_EVENT -- insert
3. MYSQL_UPDATE_ROWS_EVENT -- update
4. MYSQL_DELETE_ROWS_EVENT -- delete
事務提交后,MySQL 將執行過的 SQL 處理 BinLog Event,并持久化到 BinLog 文件
ClickHouse通過消費BinLog達到數據同步,過程中主要考慮3個方面問題:
1、DDL兼容:由于ClickHouse和MySQL的數據類型定義有區別,DDL語句需要做相應轉換
2、Delete/Update 支持:引入_version字段,控制版本信息
3、Query 過濾:引入_sign字段,標記數據有效性
DDL操作
對比一下MySQL的DDL語句以及在ClickHouse端執行的DDL語句:
mysql> show create table runoob_tbl\G; *************************** 1. row *************************** Table: runoob_tbl Create Table: CREATE TABLE `runoob_tbl` ( `runoob_id` int unsigned NOT NULL AUTO_INCREMENT, `runoob_` varchar(100) NOT NULL, `runoob_author` varchar(40) NOT NULL, `submission_date` date DEFAULT NULL, PRIMARY KEY (`runoob_id`) ) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8 1 row in set (0.00 sec) --------------------------------------------------------------- cat /metadata/slave_db/runoob_tbl.sql ATTACH TABLE _ UUID '14dbff59-930e-4aa8-9f20-ccfddaf78077' ( `runoob_id` UInt32, `runoob_` String, `runoob_author` String, `submission_date` Nullable(Date), `_sign` Int8 MATERIALIZED 1, `_version` UInt64 MATERIALIZED 1 ) ENGINE = ReplacingMergeTree(_version) PARTITION BY intDiv(runoob_id, 4294967) ORDER BY tuple(runoob_id) SETTINGS index_granularity = 8192
可以看到:
1、在DDL轉化時默認增加了2個隱藏字段:_sign(-1刪除, 1寫入) 和 _version(數據版本)
2、默認將表引擎設置為 ReplacingMergeTree,以 _version 作為 column version
3、原DDL主鍵字段 runoob_id 作為ClickHouse排序鍵和分區鍵
此外還有許多DDL處理,比如增加列、索引等,相應代碼在Parsers/MySQL 目錄下。
Delete/Update操作
Update:
# Mysql端: UPDATE runoob_tbl set runoob_author='Mike' where runoob_id=2; mysql> select * from runoob_tbl; +-----------+----------------+---------------+-----------------+ | runoob_id | runoob_title | runoob_author | submission_date | +-----------+----------------+---------------+-----------------+ | 1 | MySQL-learning | Bob | 2021-01-06 | | 2 | MySQL-learning | Mike | 2021-01-06 | +-----------+----------------+---------------+-----------------+ 2 rows in set (0.00 sec) ---------------------------------------------------------------- # ClickHouse端: DESKTOP:) select *, _sign, _version from runoob_tbl order by runoob_id; SELECT *, _sign, _version FROM runoob_tbl ORDER BY runoob_id ASC Query id: c5f4db0a-eff6-4b49-a429-b55230c26301 ┌─runoob_id─┬─runoob_title───┬─runoob_author─┬─submission_date─┬─_sign─┬─_version─┐ │ 1 │ MySQL-learning │ Bob │ 2021-01-06 │ 1 │ 2 │ │ 2 │ MySQL-learning │ Mike │ 2021-01-06 │ 1 │ 4 │ │ 2 │ MySQL-learning │ Tim │ 2021-01-06 │ 1 │ 3 │ └───────────┴────────────────┴───────────────┴─────────────────┴───────┴──────────┘ 3 rows in set. Elapsed: 0.003 sec.
Delete:
# Mysql端 mysql> DELETE from runoob_tbl where runoob_id=2; mysql> select * from runoob_tbl; +-----------+----------------+---------------+-----------------+ | runoob_id | runoob_title | runoob_author | submission_date | +-----------+----------------+---------------+-----------------+ | 1 | MySQL-learning | Bob | 2021-01-06 | +-----------+----------------+---------------+-----------------+ 1 row in set (0.00 sec) ---------------------------------------------------------------- # ClickHouse端 DESKTOP:) select *, _sign, _version from runoob_tbl order by runoob_id; SELECT *, _sign, _version FROM runoob_tbl ORDER BY runoob_id ASC Query id: e9cb0574-fcd5-4336-afa3-05f0eb035d97 ┌─runoob_id─┬─runoob_title───┬─runoob_author─┬─submission_date─┬─_sign─┬─_version─┐ │ 1 │ MySQL-learning │ Bob │ 2021-01-06 │ 1 │ 2 │ └───────────┴────────────────┴───────────────┴─────────────────┴───────┴──────────┘ ┌─runoob_id─┬─runoob_title───┬─runoob_author─┬─submission_date─┬─_sign─┬─_version─┐ │ 2 │ MySQL-learning │ Mike │ 2021-01-06 │ -1 │ 5 │ └───────────┴────────────────┴───────────────┴─────────────────┴───────┴──────────┘ ┌─runoob_id─┬─runoob_title───┬─runoob_author─┬─submission_date─┬─_sign─┬─_version─┐ │ 2 │ MySQL-learning │ Mike │ 2021-01-06 │ 1 │ 4 │ │ 2 │ MySQL-learning │ Tim │ 2021-01-06 │ 1 │ 3 │ └───────────┴────────────────┴───────────────┴─────────────────┴───────┴──────────┘ 4 rows in set. Elapsed: 0.002 sec.
可以看到,刪除id為2的行只是額外插入了_sign == -1的一行記錄,并沒有真正刪掉。
日志回放
MySQL 主從間數據同步時Slave節點將 BinLog Event 轉換成相應的SQL語句,Slave 模擬 Master 寫入。類似地,傳統第三方插件沿用了MySQL主從模式的BinLog消費方案,即將 Event 解析后轉換成 ClickHouse 兼容的 SQL 語句,然后在 ClickHouse 上執行(回放),但整個執行鏈路較長,通常性能損耗較大。不同的是,MaterializeMySQL 引擎提供的內部數據解析以及回寫方案隱去了三方插件的復雜鏈路。回放時將 BinLog Event 轉換成底層 Block 結構,然后直接寫入底層存儲引擎,接近于物理復制。此方案可以類比于將 BinLog Event 直接回放到 InnoDB 的 Page 中。
同步策略
日志回放
v20.9.1版本前是基于位點同步的,ClickHouse每消費完一批 BinLog Event,就會記錄 Event 的位點信息到 .metadata 文件:
[FavonianKong@Wsl[20:42:37]slave_db] $ cat ./.metadata Version: 2 Binlog File: mysql-bin.000003 Binlog Position:355005999 Data Version: 5
s1> ClickHouse 發送 {‘mysql-bin.000003’, 355005999} 位點信息給 MySQL s2> MySQL 找到本地 mysql-bin.000003 文件并定位到 355005999 偏移位置,讀取下一個 Event 發送給 ClickHouse s3> ClickHouse 接收 binlog event 并完成同步操作 s4> ClickHouse 更新 .metadata位點
存在問題:
如果MySQL Server是一個集群,通過VIP對外服務,MaterializeMySQL創建 database 時 host 指向的是VIP,當集群主從發生切換后,{Binlog File, Binlog Position} 二元組不一定是準確的,因為BinLog可以做reset操作。
s1> ClickHouse 發送 {'mysql-bin.000003’, 355005999} 給集群新主 MySQL s2> 新主 MySQL 發現本地沒有 mysql-bin.000003 文件,因為它做過 reset master 操作,binlog 文件是 mysql-bin.000001 s3> 產生錯誤復制
為了解決這個問題,v20.9.1版本后上線了 GTID 同步模式,廢棄了不安全的位點同步模式。
GTID同步
GTID模式為每個 event 分配一個全局唯一ID和序號,直接告知 MySQL 這個 GTID 即可,于是.metadata變為:
[FavonianKong@Wsl[21:30:19]slave_db] Version: 2 Binlog File: mysql-bin.000003 Executed GTID: 0857c24e-4755-11eb-888c-00155dfbdec7:1-783 Binlog Position:355005999 Data Version: 5
其中 0857c24e-4755-11eb-888c-00155dfbdec7 是生成 Event的主機UUID,1-783是已經同步的event區間
于是流程變為:
s1> ClickHouse 發送 GTID:0857c24e-4755-11eb-888c-00155dfbdec7:1-783 給 MySQL s2> MySQL 根據 GTID 找到本地位點,讀取下一個 Event 發送給 ClickHouse s3> ClickHouse 接收 BinLog Event 并完成同步操作 s4> ClickHouse 更新 .metadata GTID信息
源碼分析
概述
在最新源碼 (v20.13.1.1) 中,ClickHouse 官方對 DatabaseMaterializeMySQL 引擎的相關源碼進行了重構,并適配了 GTID 同步模式。ClickHouse 整個項目的入口 main 函數在 /ClickHouse/programs/main.cpp 文件中,主程序會根據接收指令將任務分發到 ClickHouse/programs 目錄下的子程序中處理。本次分析主要關注 Server 端 MaterializeMySQL 引擎的工作流程。
源碼目錄
與 MaterializeMySQL 相關的主要源碼路徑:
ClickHouse/src/databases/MySQL //MaterializeMySQL存儲引擎實現 ClickHouse/src/Storages/ //表引擎實現 ClickHouse/src/core/MySQL* //復制相關代碼 ClickHouse/src/Interpreters/ //Interpreters實現,SQL的rewrite也在這里處理 ClickHouse/src/Parsers/MySQL //解析部分實現,DDL解析等相關處理在這里
服務端主要流程
ClickHouse 使用 POCO 網絡庫處理網絡請求,Client連接的處理邏輯在 ClickHouse/src/Server/*Handler.cpp 的 hander方法里。以TCP為例,除去握手,初始化上下文以及異常處理等相關代碼,主要邏輯可以抽象成:
// ClickHouse/src/Server/TCPHandler.cpp TCPHandler.runImpl() { ... while(true) { ... if (!receivePacket()) //line 184 continue /// Processing Query //line 260 state.io = executeQuery(state.query, *query_context, ...); ... }
數據同步預處理
Client發送的SQL在executeQuery函數處理,主要邏輯簡化如下:
// ClickHouse/src/Interpreters/executeQuery.cpp static std::tuple executeQueryImpl(...) { ... // line 354,解析器可配置 ast = parseQuery(...); ... // line 503, 根據語法樹生成interpreter auto interpreter = InterpreterFactory::get(ast, context, ...); ... // line 525, 執行器interpreter執行后返回結果 res = interpreter->execute(); ... }
主要有三點:
1、解析SQL語句并生成語法樹 AST
2、InterpreterFactory 工廠類根據 AST 生成執行器
3、interpreter->execute()
跟進第三點,看看 InterpreterCreateQuery 的 excute() 做了什么:
// ClickHouse/src/Interpreters/InterpreterCreateQuery.cpp BlockIO InterpreterCreateQuery::execute() { ... // CREATE | ATTACH DATABASE if (!create.database.empty() && create.table.empty()) // line 1133, 當使用MaterializeMySQL時,會走到這里建庫 return createDatabase(create); }
這里注釋很明顯,主要執行 CREATE 或 ATTACH DATABASE,繼續跟進 createDatabase() 函數:
// ClickHouse/src/Interpreters/InterpreterCreateQuery.cpp BlockIO InterpreterCreateQuery::createDatabase(ASTCreateQuery & create) { ... // line 208, 這里會根據 ASTCreateQuery 參數,從 DatabaseFactory 工廠獲取數據庫對象 // 具體可以參考 DatabasePtr DatabaseFactory::getImpl() 函數 DatabasePtr database = DatabaseFactory::get(create, metadata_path, ...); ... // line 253, 多態調用,在使用MaterializeMySQL時 // 上方get函數返回的是 DatabaseMaterializeMySQL database->loadStoredObjects(context, ...); }
到這里,相當于將任務分發給DatabaseMaterializeMySQL處理,接著跟蹤 loadStoredObjects 函數:
//ClickHouse/src/Databases/MySQL/DatabaseMaterializeMySQL.cpp template void DatabaseMaterializeMySQL::loadStoredObjects(Context & context, ...) { Base::loadStoredObjects(context, has_force_restore_data_flag, force_attach); try { // line87, 這里啟動了materialize的同步線程 materialize_thread.startSynchronization(); started_up = true; } catch (...) ... }
跟進startSynchronization() 綁定的執行函數:
// ClickHouse/src/Databases/MySQL/MaterializeMySQLSyncThread.cpp void MaterializeMySQLSyncThread::synchronization() { ... // 全量同步在 repareSynchronized() 進行 if (std::optional metadata = prepareSynchronized()) { while (!isCancelled()) { UInt64 max_flush_time = settings->max_flush_data_time; BinlogEventPtr binlog_event = client.readOneBinlogEvent(...); { //增量同步偵聽binlog_envent if (binlog_event) onEvent(buffers, binlog_event, *metadata); } } } ... }
全量同步
MaterializeMySQLSyncThread::prepareSynchronized 負責DDL和全量同步,主要流程簡化如下:
// ClickHouse/src/Databases/MySQL/MaterializeMySQLSyncThread.cpp std::optional MaterializeMySQLSyncThread::prepareSynchronized() { while (!isCancelled()) { ... try { //構造函數內會獲取MySQL的狀態、MySQL端的建表語句, MaterializeMetadata metadata(connection, ...); // line345, DDL相關轉換 metadata.transaction(position, [&]() { cleanOutdatedTables(database_name, global_context); dumpDataForTables(connection, metadata, global_context, ...); }); return metadata; } ... } }
ClickHouse作為MySQL從節點,在MaterializeMetadata構造函數中對MySQL端進行了一系列預處理:
1、將打開的表關閉,同時對表加上讀鎖并啟動事務
2、TablesCreateQuery通過SHOW CREATE TABLE 語句獲取MySQL端的建表語句
3、獲取到建表語句后釋放表鎖
繼續往下走,執行到 metadata.transaction() 函數,該調用傳入了匿名函數作為參數,一直跟進該函數會發現最終會執行匿名函數,也就是cleanOutdatedTables以及dumpDataForTables函數,主要看一下 dumpDataForTables 函數:
// ClickHouse/src/Databases/MySQL/MaterializeMySQLSyncThread.cpp static inline void dumpDataForTables(...) { ... //line293, 這里執行建表語句 tryToExecuteQuery(..., query_context, database_name, comment); }
// ClickHouse/src/Interpreters/executeQuery.cpp static std::tuple executeQueryImpl(...) { ... // line 354,解析器可配置 ast = parseQuery(...); ... // line 503,這里跟之前上下文信息不同,生成interpreter也不同 auto interpreter = InterpreterFactory::get(ast,context, ...); ... // line 525, 執行器interpreter執行后返回結果 res = interpreter->execute(); ... }
// ClickHouse/src/Interpreters/InterpreterExternalDDLQuery.cpp BlockIO InterpreterExternalDDLQuery::execute() { ... if (external_ddl_query.from->name == "MySQL") { #ifdef USE_MYSQL ... // line61, 當全量復制執行DDL時,會執行到這里 else if (...->as()) return MySQLInterpreter::InterpreterMySQLCreateQuery( external_ddl_query.external_ddl, cogetIdentifierName(arguments[0]), getIdentifierName(arguments[1])).execute(); #endif } ... return BlockIO(); }
// ClickHouse/src/Interpreters/MySQL/InterpretersMySQLDDLQuery.h class InterpreterMySQLDDLQuery : public IInterpreter { public: ... BlockIO execute() override { ... // line68, 把從MySQL獲取到的DDL語句進行轉化 ASTs rewritten_queries = InterpreterImpl::getRewrittenQueries( query, context, mapped_to_database, mysql_database); // line70, 這里執行轉化后的DDL語句 for (const auto & rewritten_query : rewritten_queries) executeQuery(..., queryToString(rewritten_query), ...); return BlockIO{}; } ... }
// ClickHouse/src/Interpreters/MySQL/InterpretersMySQLDDLQuery.cpp ASTs InterpreterCreateImpl::getRewrittenQueries(...) { ... // 檢查是否存在primary_key, 沒有直接報錯 if (primary_keys.empty()) throw Exception("cannot be materialized, no primary keys.", ...); ... // 添加 _sign 和 _version 列. auto sign_column_name = getUniqueColumnName(columns_name_and_type, "_sign"); auto version_column_name = getUniqueColumnName(columns_name_and_type, "_version"); // 這里悄悄把建表引擎修改成了ReplacingMergeTree storage->set(storage->engine, makeASTFunction("ReplacingMergeTree", ...)); ... return ASTs{rewritten_query}; }
// ClickHouse/src/Databases/MySQL/MaterializeMySQLSyncThread.cpp static inline void dumpDataForTables(...) { ... //line293, 這里執行建表語句 tryToExecuteQuery(..., query_context, database_name, comment); ... // line29, 這里開始 dump 數據并存放到MySQLBlockInputStream MySQLBlockInputStream input(connection, ...); }
增量同步
還記得startSynchronization() 綁定的執行函數嗎?全量同步分析都是在 prepareSynchronized()進行的,那增量更新呢?
// ClickHouse/src/Databases/MySQL/MaterializeMySQLSyncThread.cpp void MaterializeMySQLSyncThread::synchronization() { ... // 全量同步在 repareSynchronized() 進行 if (std::optional metadata = prepareSynchronized()) { while (!isCancelled()) { UInt64 max_flush_time = settings->max_flush_data_time; BinlogEventPtr binlog_event = client.readOneBinlogEvent(...); { //增量同步偵聽binlog_envent if (binlog_event) onEvent(buffers, binlog_event, *metadata); } } } ... }
可以看到,while 語句里有一個 binlog_event 的偵聽函數,用來偵聽 MySQL 端 BinLog 日志變化,一旦 MySQL 端執行相關操作,其 BinLog 日志會更新并觸發 binlog_event,增量更新主要在這里進行。
// ClickHouse/src/Databases/MySQL/MaterializeMySQLSyncThread.cpp void MaterializeMySQLSyncThread::onEvent(Buffers & buffers, const BinlogEventPtr & receive_event, MaterializeMetadata & metadata) { // 增量同步通過監聽binlog event實現,目前支持四種event:MYSQL_WRITE_ROWS_EVENT、 // MYSQL_UPDATE_ROWS_EVENT、MYSQL_DELETE_ROWS_EVENT 和 MYSQL_QUERY_EVENT // 具體的流程可以查找對應的 onHandle 函數, 不在此詳細分析 if (receive_event->type() == MYSQL_WRITE_ROWS_EVENT){...} else if (receive_event->type() == MYSQL_UPDATE_ROWS_EVENT){...} else if (receive_event->type() == MYSQL_DELETE_ROWS_EVENT){...} else if (receive_event->type() == MYSQL_QUERY_EVENT){...} else {/* MYSQL_UNHANDLED_EVENT*/} }
小結
MaterializeMySQL 引擎是 ClickHouse 官方2020年主推的特性,由于該特性在生產環境中屬于剛需且目前剛上線不久,整個模塊處于高速迭代的狀態,因此有許多待完善的功能。例如復制過程狀態查看以及數據的一致性校驗等。感興趣的話可參考Github上的2021-Roadmap,里面會更新一些社區最近得計劃。以上內容如有理解錯誤還請指正。
引用
ClickHouse社區文檔
ClickHouse社區源碼
MySQL實時復制與實現
MaterializeMySQL引擎分析
ClickHouse MySQL SQL
版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。
版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。