《Replication的犄角旮旯》系列導讀
Replication的犄角旮旯(一)--變更訂閱端表名的應用場景
Replication的犄角旮旯(二)--尋找訂閱端丟失的記錄
Replication的犄角旮旯(三)--聊聊@bitmap
Replication的犄角旮旯(四)--關於事務復制的監控
Replication的犄角旮旯(五)--關於復制identity列
Replication的犄角旮旯(六)-- 一個DDL引發的血案(上)(如何近似估算DDL操作進度)
Replication的犄角旮旯(七)-- 一個DDL引發的血案(下)(聊聊logreader的延遲)
Replication的犄角旮旯(八)-- 訂閱與發布異構的問題
Replication的犄角旮旯(九)-- sp_setsubscriptionxactseqno,賦予訂閱活力的工具
---------------------------------------華麗麗的分割線--------------------------------------------
前言:有人總是拿Mysql的Master\Slave和SQL Server的replication比較,說Mysql的復制有多么強大、多么靈活。作為SQLServer的死忠,也曾被replication各種 的黑盒搞得體無完膚。不過還好,我們還是能從MS流露出來的各種存儲過程中,發現蛛絲馬跡,結合我們的頭腦風暴,來一場真真正正的革命……
sp_setsubscriptionxactseqno,第一次了解是在拜讀前任DBR的blog《在SQL Server 2005/2008事務復制中如何跳過一個事務》時,而近期在處理一個復制異常事件時,忽然靈光閃現,既然可以向后跳過某些事務,是否可以前滾到之前的某個時間點再繼續復制呢?本文將通過實際測試,繼續玩復制
閑話少敘,書歸正傳……
關於跳過某些復制事務,在此不再贅述,詳見《在SQL Server 2005/2008事務復制中如何跳過一個事務》;這里只說如何追溯到之前某個時間點;
關於sp_setsubscriptionxactseqno這個存儲過程,詳見MSDN:https://msdn.microsoft.com/zh-cn/library/ms188764.aspx
具體用法如下:
sp_setsubscriptionxactseqno [ @publisher = ] 'publisher'
, [ @publisher_db = ] 'publisher_db'
, [ @publication = ] 'publication'
, [ @xact_seqno = ] xact_seqno
其中@xact_seqno這個參數,如果指定當前事務(起始點)之后的某個事務(截止點),就可以跳過兩個時間點之間的事務;但如果需要跳到當前事務之前的某個事務時,除了sp_setsubscriptionxactseqno這個存儲過程外,還需要一個系統表來配合才能實現——MSsubscriptions,位於分發庫(默認為distribution)中;
MSsubscriptions記錄了每個訂閱與發布項目的關系,詳見MSDN:https://msdn.microsoft.com/zh-cn/library/ms188368.aspx
其中publisher_seqno表示該訂閱創建時在發布服務器上的事務序列號,subscription_seqno為快照事務序列號(非初始化訂閱則與publisher_seqno一致);
在事務復制中,訂閱端應用事務時,需要檢測當前事務是否小於這兩個參數,如果當前事務號小於上述兩個列的值,則邏輯上判為不成立(當前執行的事務早於創建訂閱時的事務,理論上不成立)。
因此,要想回跳到之前某個時間點的事務,需要手動更新相應的記錄,至少保證publisher_seqno和subscription_seqno與你要回跳的那個事務號一致;
至此,我們目的達成,但這又有什么意義呢?當前時間點下的數據為什么要回跳到之前的某個事務呢?這就要說到前幾天我們處理的一個案例;
先說一下我們的復制環境,以下圖為例:
根節點為寫庫,承載主寫業務,為避免單點故障,增加一災備節點進行保護;
轉發節點作為根節點的訂閱以及末端節點的發布,只進行復制命令的轉發工作,雙轉發進行冗余,避免單鏈路故障影響末端訂閱;
末端訂閱承載各類讀業務,雙鏈路各取部分節點做負載均衡;
看似健壯的架構,避免的單點問題。但仍有個隱患,當任意一個轉發節點故障時,盡管可以隨時刪除復制鏈路(末端訂閱有負載均衡保護,隨時可以刪除節點),但由於復制的基礎數據過大,故障回復時,無論是快照還是備份初始化,時間成本都很高。怎么破?
此前出現了"轉發節點02"因增加存儲空間導致開機后出現邏輯頁錯誤,致使該條鏈路失效,難道真的需要重新初始化才能恢復?(需要重建3個節點,轉發節點02、末端訂閱03、04)
或許我們有更好的辦法可以避免初始化的過程;
以下為本地測試的情況:
思路:啟用災備節點作為轉發節點的基礎數據
難點:
1、災備節點數據如何恢復到故障時完整銜接中斷的復制事務?
2、如果能保證事務完美銜接但數據超前,如何處理redo部分的沖突,並使其正常分發到下級訂閱?
解決方法:
1、利用現有災備節點,可以快速實現數據超前於故障時間點;
2、通過sp_setsubscriptionxactseqno可以重置災備節點的訂閱事務號,使其從較早的事務進行redo;但同時需要修改[distribution].[dbo].[MSsubscriptions]中的publisher_seqno、subscription_seqno,使其生效;
3、由於災備節點數據超前於下級訂閱節點數據,需考慮對災備節點及下級訂閱節點中,訂閱表存儲過程的修正,實現自動redo的過程;
測試環境:
根節點:BJYW-XIAOLEI\SQL01 testDB_A庫(SQL2012)
轉發節點:BJYW-XIAOLEI\SQL02 testDB_A庫(SQL2008 R2)
末端訂閱:BJYW-XIAOLEI\SQL02 testDB_B庫(SQL2008 R2)
災備節點:BJYW-XIAOLEI\SQL01 testDB_C庫(SQL2012)
測試過程:
1、創建測試表及測試數據
1 USE [testDB_A] 2 3 GO 4 5 CREATE TABLE [dbo].[test_a]( 6 7 [id] [int] IDENTITY(1,1) NOT FOR REPLICATION NOT NULL, 8 9 [context] [varchar](100) NULL, 10 11 PRIMARY KEY CLUSTERED 12 13 ( 14 15 [id] ASC 16 17 )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] 18 19 ) ON [PRIMARY] 20 21 GO 22 23 INSERT INTO dbo.test_a( context )VALUES ( '00001' ) 24 25 GO 10
2、創建復制鏈路
BJYW-XIAOLEI\SQL01 testDB_A --> BJYW-XIAOLEI\SQL02 testDB_A --> BJYW-XIAOLEI\SQL02 testDB_B
災備:BJYW-XIAOLEI\SQL01 testDB_C庫(SQL2012)使用同樣的測試腳本及數據;
復制關系如下:
3、模擬故障
在轉發節點(BJYW-XIAOLEI\SQL02 testDB_A)刪除一條數據后,在根節點上更新該數據
轉發節點:DELETE testDB_A.dbo.test_a WHERE id=10
根節點:UPDATE testDB_A.dbo.test_a SET context='delete' WHERE id=10
檢查msrepl_errors,發現報錯
4、主寫和災備節點再插入10條數據,用於模擬故障發生后,災備節點數據超前於末端訂閱節點
1 INSERT INTO dbo.test_a( context )VALUES ( '00002' ) 2 GO 10
5、創建主寫節點到災備節點的不初始化訂閱關系,並停止分發代理作業;同時創建災備節點到末端訂閱的不初始化復制鏈路;
SQL01的testDB_A庫新建到災備節點(testDB_C)的不初始化訂閱

SQL01的testDB_C庫新建到末端訂閱(testDB_B)的不初始化訂閱

6、修改災備節點的初始訂閱事務號為故障點的事務號
找到最早出錯的事務號(見第3步):0x0000004300000040000300000000
1 USE testDB_C 2 GO 3 4 sp_setsubscriptionxactseqno @publisher = 'BJYW-XIAOLEI\SQL01' 5 , @publisher_db = 'testdb_a' 6 , @publication = 'SQL01_A' 7 , @xact_seqno = 0x0000004300000040000300000000 8 9 USE distribution 10 GO 11 12 SELECT SP.publisher_seqno,SP.subscription_seqno,* FROM [distribution].[dbo].[MSsubscriptions] SP JOIN dbo.MSpublications PB ON SP.publisher_id=PB.publisher_id 13 AND SP.publisher_db=PB.publisher_db AND SP.publication_id=PB.publication_id 14 WHERE PB.publisher_db='testDB_A' AND pb.publication='SQL01_A' AND sp.subscriber_db='testDB_C' 15 16 17 18 UPDATE SP 19 SET SP.publisher_seqno= 0x0000004300000040000300000000 ,SP.subscription_seqno=0x0000004300000040000300000000 20 FROM [distribution].[dbo].[MSsubscriptions] SP JOIN dbo.MSpublications PB ON SP.publisher_id=PB.publisher_id 21 AND SP.publisher_db=PB.publisher_db AND SP.publication_id=PB.publication_id 22 WHERE PB.publisher_db='testDB_A' AND pb.publication='SQL01_A' AND sp.subscriber_db='testDB_C'
修改后的記錄
7、修改災備節點的訂閱存儲過程
a) insert:判斷主鍵是否存在,如存在,需刪除后再insert;
b) update:對非主要鍵值,建議先set col=null,再set col='',最后再執行正確的update;
c) delete :暫不處理,只記錄主鍵信息,后續處理;
Inster存儲過程

Update存儲過程
8、修改末端訂閱的訂閱存儲過程
a) insert:判斷主鍵是否存在,如存在,需刪除后再insert;
b) update:記錄不存在主鍵的記錄,后續從log表中補足;
c) delete :注釋判斷if @@rowcount=0的部分,不報錯即可;
9、為方便監控,創建相應的trigger抓取實際操作情況;
1 create TRIGGER [dbo].[tri_del_test_a] 2 ON [dbo].[test_a] 3 AFTER DELETE 4 AS 5 BEGIN 6 SET NOCOUNT ON; 7 INSERT INTO monitor.dbo.trigger_monitor_byxl(tbname,t_type,t_VALUE,checktime) 8 SELECT 'test_a','delete','id= '+CAST(ID AS VARCHAR(10)),GETDATE() FROM DELETED 9 END 10 11 12 13 create TRIGGER [dbo].[tri_upd_test_a] 14 ON [dbo].[test_a] 15 AFTER UPDATE 16 AS 17 BEGIN 18 SET NOCOUNT ON; 19 INSERT INTO monitor.dbo.trigger_monitor_byxl(tbname,t_type,t_VALUE,checktime) 20 SELECT 'test_a','update','id= '+CAST(ID AS VARCHAR(10))+'- context='+context,GETDATE() FROM DELETED 21 END
10、啟用災備節點對應的分發代理;
由於之前中間節點采用delete的方式刪除了數據,此處為了末端訂閱恢復正常,手動insert原記錄;
實際生產環境中,可以根據故障出現時暫停的事務,手動處理一個事務;后續事務正常應用到末端訂閱
注意:
由於在將災備節點轉為訂閱節點過程中,創建訂閱存儲過程的命令會記錄到msrepl_commands中,因此,在跨過創建訂閱的事務時,會將存儲過程重置為初始狀態;
另一方面,由於寫庫不停機,在將災備節點轉為訂閱節點時至手動停止分發代理過程中,可能存在少量數據寫入,因而在存儲過程重置后,由於數據少量超前,會導致主鍵沖突(insert)的問題,而update可能丟失(set值前后一樣的情況,不會出現臟數據,則不會記錄到復制命令中);
建議:在將災備節點轉為訂閱節點時,代理計划部分改為“按需執行”
