閱讀283 返回首頁    go 技術社區[雲棲]


數據庫平滑switchover的要素 - 會話資源漂移

標簽

PostgreSQL , 中間件 , 連接池 , 平滑切換 , 會話設置 , 會話狀態 , 綁定變量語句 , prepared statement


背景

數據庫遷移、切換是很普遍的一個話題,但是大多數的方案,對用戶來說都是有感知的,所以用戶的體驗並不好。

例如用戶使用了綁定變量語句,主備角色切換後綁定變量語句沒有了,會導致調用報錯。

我們需要維護主庫的硬件,那麼可以在中間件層麵,將主備數據庫的角色進行平滑調換,維護好硬件,再平滑的調換回來。

數據庫主備切換時,如何做到會話級無感知?首先我們要了解會話中都有哪些內容,哪些內容是需要隨角色切換一起遷移的。從而做到用戶無感知。

(本文HA指中間件層級的HA,並非APP直連數據庫,VIP切換的那種HA。)

簡單的switchover過程舉例:

等待所有會話的事務結束,會話都處於idle狀態,凍結會話,不允許提交任何SQL,然後進行角色切換,並將每個會話的資源狀態平移。

會話資源狀態

會話中有些什麼狀態?通過discard這條SQL就可以了解。

https://www.postgresql.org/docs/10/static/sql-discard.html

DISCARD — discard session state  

discard all相當於執行如下

SET SESSION AUTHORIZATION DEFAULT;  
RESET ALL;  
DEALLOCATE ALL;  
CLOSE ALL;  
UNLISTEN *;  
SELECT pg_advisory_unlock_all();  
DISCARD PLANS;  
DISCARD SEQUENCES;  
DISCARD TEMP;  

會話資源中目前可能包含如下(每個PG版本可能有些許差異):

SESSOIN角色、參數設置、綁定變量語句、遊標、異步消息監聽、AD鎖、序列、臨時表等。

下麵介紹一下每種資源的查詢方法,以及在新的主庫上進行資源複原的方法。

一、SESSION AUTHORIZATION

超級用戶可以將會話用戶設置為其他用戶,普通用戶無權切換用戶。

當前用戶為postgres,設置SESSION AUTHORIZATION為test

postgres=# set SESSION AUTHORIZATION test;  
SET  
postgres=> show SESSION AUTHORIZATION;  
 session_authorization   
-----------------------  
 test  
(1 row)  
postgres=> select usename from pg_stat_activity where pid=pg_backend_pid();  
 usename    
----------  
 postgres  
(1 row)  

查詢方法

postgres=> show SESSION AUTHORIZATION;  
 session_authorization   
-----------------------  
 test  
(1 row)  
postgres=> select usename from pg_stat_activity where pid=pg_backend_pid();  
 usename    
----------  
 postgres  
(1 row)  

複原方法

當pg_stat_activity.usename不等於SESSION AUTHORIZATION時,需要通過如下方法複原它。

postgres=# set SESSION AUTHORIZATION test;  
SET  

二、參數

PostgreSQL的一些參數是允許用戶在會話、事務中進行設置的。如下context in ('user','superuser')時,用戶可以在會話或事務中設置。

postgres=# select distinct context from pg_settings ;  
      context        
-------------------  
 superuser-backend  
 sighup  
 superuser  
 postmaster  
 internal  
 user  
 backend  
(7 rows)  

設置例子

postgres=> set tcp_keepalives_count=1;  
SET  
  
source表示參數來自哪裏的設置,如果來自會話或事務級設置,則顯示session  
  
postgres=> select distinct source from pg_settings ;  
        source          
----------------------  
 session  
 default  
 command line  
 configuration file  
 client  
 override  
 environment variable  
(7 rows)  
  
重置方法  
  
postgres=# reset tcp_keepalives_count;  
RESET  
postgres=# select name,setting,reset_val,source,context from pg_settings where name='tcp_keepalives_count';  
         name         | setting | reset_val | source  | context   
----------------------+---------+-----------+---------+---------  
 tcp_keepalives_count | 3       | 0         | default | user  
(1 row)  

查詢方法

postgres=# select name,setting,reset_val,source,context from pg_settings where source ='session' and setting<>reset_val;  
         name         | setting | reset_val | source  | context   
----------------------+---------+-----------+---------+---------  
 tcp_keepalives_count | 1       | 0         | session | user  
(1 row)  

複原方法

postgres=> set tcp_keepalives_count=1;  
SET  

三、綁定變量語句

使用綁定變量可以減少數據庫的parser, plan開銷,提高高並發的查詢性能,同時避免SQL注入。

不同的驅動,有不同的使用方法。

https://www.postgresql.org/docs/10/static/libpq-exec.html

使用綁定變量的例子

CREATE OR REPLACE FUNCTION public.getps()  
 RETURNS void  
 LANGUAGE plpgsql  
 STRICT  
AS $function$  
declare   
  rec record;  
begin  
  for rec in select t from pg_prepared_statements t loop  
    raise notice '%', (rec.*)::text;  
  end loop;  
end;  
$function$;  
  
  
create table ps(id int primary key, info text);  
insert into ps select generate_series(1,10000), 'test';  
  
  
vi test.sql  
  
\set id random(1,10000)  
select * from ps where id=:id;  
select getps();  
  
使用綁定變量的模式,調用SQL  
  
pgbench -M prepared -n -r -P 1 -f ./test.sql -c 1 -j 1  
  
NOTICE:  ("(P0_1,""select * from ps where id=$1;"",""2017-06-19 15:22:21.821454+08"",{integer},f)")  
NOTICE:  ("(P0_2,""select getps();"",""2017-06-19 15:22:21.822045+08"",{},f)")  
.....  

查詢方法

postgres=# \d pg_prepared_statements  
                  View "pg_catalog.pg_prepared_statements"  
     Column      |           Type           | Collation | Nullable | Default   
-----------------+--------------------------+-----------+----------+---------  
 name            | text                     |           |          |   
 statement       | text                     |           |          |   
 prepare_time    | timestamp with time zone |           |          |   
 parameter_types | regtype[]                |           |          |   
 from_sql        | boolean                  |           |          |   
  
postgres=# select * from pg_prepared_statements;  
 name | statement | prepare_time | parameter_types | from_sql   
------+-----------+--------------+-----------------+----------  
(0 rows)  
  
postgres=# prepare a(int) as select * from ps where id=$1;  
PREPARE  
postgres=# execute a(1);  
 id | info   
----+------  
  1 | test  
(1 row)  
  
postgres=# select * from pg_prepared_statements;  
 name |                    statement                    |         prepare_time         | parameter_types | from_sql   
------+-------------------------------------------------+------------------------------+-----------------+----------  
 a    | prepare a(int) as select * from ps where id=$1; | 2017-06-19 15:23:24.68617+08 | {integer}       | t  
(1 row)  

複原方法

不同的驅動,複原方法不一樣。

請根據pg_prepared_statements的內容進行複原。

NOTICE:  ("(P0_1,""select * from ps where id=$1;"",""2017-06-19 15:22:21.821454+08"",{integer},f)")  
  
PGresult *PQprepare(PGconn *conn,  
                    const char *stmtName,  
                    const char *query,  
                    int nParams,  
                    const Oid *paramTypes);  

四、遊標

如果我們使用了hold選項,那麼遊標不會隨事務結束而關閉,因此在遷移會話時也需要注意是否有這類遊標。

postgres=# \h declare  
Command:     DECLARE  
Description: define a cursor  
Syntax:  
DECLARE name [ BINARY ] [ INSENSITIVE ] [ [ NO ] SCROLL ]  
    CURSOR [ { WITH | WITHOUT } HOLD ] FOR query  
  
postgres=# begin;  
BEGIN  
postgres=# declare cur cursor with hold for select * from ps where id=1;  
DECLARE CURSOR  
postgres=# end;  
COMMIT  
postgres=# select * from pg_cursors ;  
 name |                           statement                           | is_holdable | is_binary | is_scrollable |         creation_time           
------+---------------------------------------------------------------+-------------+-----------+---------------+-------------------------------  
 cur  | declare cur cursor with hold for select * from ps where id=1; | t           | f         | t             | 2017-06-19 15:27:58.604183+08  
(1 row)  
  
postgres=# close cur;  
CLOSE CURSOR  
postgres=# select * from pg_cursors ;  
 name | statement | is_holdable | is_binary | is_scrollable | creation_time   
------+-----------+-------------+-----------+---------------+---------------  
(0 rows)  
  

查詢方法

postgres=# select * from pg_cursors ;  
 name |                           statement                           | is_holdable | is_binary | is_scrollable |         creation_time           
------+---------------------------------------------------------------+-------------+-----------+---------------+-------------------------------  
 cur  | declare cur cursor with hold for select * from ps where id=1; | t           | f         | t             | 2017-06-19 15:27:58.604183+08  
(1 row)  

複原方法

postgres=# declare cur cursor with hold for select * from ps where id=1;  
DECLARE CURSOR  

五、異步消息監聽

PostgreSQL的異步消息,可以通過異步消息,推送事件。例子如下:

https://www.postgresql.org/docs/10/static/libpq-notify.html

https://www.postgresql.org/docs/10/static/libpq-example.html

postgres=# listen a;  
LISTEN  
postgres=# notify a , 'hello i am digoal';  
NOTIFY  
Asynchronous notification "a" with payload "hello i am digoal" received from server process with PID 21412.  

查詢方法

查詢已經開啟了哪些異步監聽

postgres=# select pg_listening_channels();  
 pg_listening_channels   
-----------------------  
 a  
(1 row)  

複原方法

postgres=# listen a;  
LISTEN  

六、advisory lock

advisory lock可以用於秒殺、解決高並發鎖衝突問題、解決無空洞序列值問題等。

《PostgreSQL 使用advisory lock實現行級讀寫堵塞》

《PostgreSQL 無縫自增ID的實現 - by advisory lock》

《PostgreSQL 使用advisory lock或skip locked消除行鎖衝突, 提高幾十倍並發更新效率》

《聊一聊雙十一背後的技術 - 不一樣的秒殺技術, 裸秒》

postgres=# \df *.*advis*  
                                        List of functions  
   Schema   |               Name               | Result data type | Argument data types |  Type    
------------+----------------------------------+------------------+---------------------+--------  
 pg_catalog | pg_advisory_lock                 | void             | bigint              | normal  
 pg_catalog | pg_advisory_lock                 | void             | integer, integer    | normal  
 pg_catalog | pg_advisory_lock_shared          | void             | bigint              | normal  
 pg_catalog | pg_advisory_lock_shared          | void             | integer, integer    | normal  
 pg_catalog | pg_advisory_unlock               | boolean          | bigint              | normal  
 pg_catalog | pg_advisory_unlock               | boolean          | integer, integer    | normal  
 pg_catalog | pg_advisory_unlock_all           | void             |                     | normal  
 pg_catalog | pg_advisory_unlock_shared        | boolean          | bigint              | normal  
 pg_catalog | pg_advisory_unlock_shared        | boolean          | integer, integer    | normal  
 pg_catalog | pg_advisory_xact_lock            | void             | bigint              | normal  
 pg_catalog | pg_advisory_xact_lock            | void             | integer, integer    | normal  
 pg_catalog | pg_advisory_xact_lock_shared     | void             | bigint              | normal  
 pg_catalog | pg_advisory_xact_lock_shared     | void             | integer, integer    | normal  
 pg_catalog | pg_try_advisory_lock             | boolean          | bigint              | normal  
 pg_catalog | pg_try_advisory_lock             | boolean          | integer, integer    | normal  
 pg_catalog | pg_try_advisory_lock_shared      | boolean          | bigint              | normal  
 pg_catalog | pg_try_advisory_lock_shared      | boolean          | integer, integer    | normal  
 pg_catalog | pg_try_advisory_xact_lock        | boolean          | bigint              | normal  
 pg_catalog | pg_try_advisory_xact_lock        | boolean          | integer, integer    | normal  
 pg_catalog | pg_try_advisory_xact_lock_shared | boolean          | bigint              | normal  
 pg_catalog | pg_try_advisory_xact_lock_shared | boolean          | integer, integer    | normal  
(21 rows)  

advisory lock分為事務級鎖和會話級鎖,在會話遷移時,會話處於IDLE狀態, 隻需要關注會話級鎖。

postgres=# select pg_try_advisory_lock(1);  
 pg_try_advisory_lock   
----------------------  
 t  
(1 row)  

查詢方法

postgres=# select * from pg_locks where locktype='advisory' and pid=pg_backend_pid();  
 locktype | database | relation | page | tuple | virtualxid | transactionid | classid | objid | objsubid | virtualtransaction |  pid  |     mode      | granted | fastpath   
----------+----------+----------+------+-------+------------+---------------+---------+-------+----------+--------------------+-------+---------------+---------+----------  
 advisory |    13158 |          |      |       |            |               |       0 |     1 |        1 | 3/123301864        | 21412 | ExclusiveLock | t       | f  
(1 row)  

複原方法

注意複原時,需要指定是否為shared lock。

postgres=# select pg_try_advisory_lock(1);  
 pg_try_advisory_lock   
----------------------  
 t  
(1 row)  

七、序列

序列使用後,會在會話中存儲最後一次使用的序列的VAL,以及每個序列被使用後的最後一次獲取的VAL。

postgres=# create sequence seq1;  
CREATE SEQUENCE  
  
沒有被調用的序列,返回錯誤。  
postgres=# select currval('seq');  
ERROR:  currval of sequence "seq" is not yet defined in this session  
  
沒有調用過任何序列,返回錯誤。  
postgres=# select lastval();  
ERROR:  lastval is not yet defined in this session  
  
調用序列  
postgres=# select nextval('seq1');  
 nextval   
---------  
       1  
(1 row)  
  
返回會話中指定序列最後一次調用的VAL  
postgres=# select currval('seq1');  
 currval   
---------  
       1  
(1 row)  
  
返回整個會話中最後一次序列調用的VAL  
postgres=# select lastval();  
 lastval   
---------  
       1  
(1 row)  

查詢方法

postgres=# select * from seq1;  
 last_value | log_cnt | is_called   
------------+---------+-----------  
          1 |      32 | t  
(1 row)  
postgres=# select nextval('seq1');  
 nextval   
---------  
       2  
(1 row)  
  
postgres=# select * from seq1;  
 last_value | log_cnt | is_called   
------------+---------+-----------  
          2 |      31 | t  
(1 row)  

複原方法

序列雖然可以設置當前值,但是會影響全局,強烈建議不要這麼做。

目前沒有好的方法複原序列在會話中的lastval。

八、臨時表

postgres=# create temp table tmp(id int, info text);  
CREATE TABLE  
  
postgres=# select oid,relname from pg_class where relpersistence ='t' and relkind='r' and pg_table_is_visible(oid);  
  oid  | relname   
-------+---------  
 44804 | tmp  
(1 row)  

查詢方法

postgres=# select oid,relname from pg_class where relpersistence ='t' and relkind='r' and pg_table_is_visible(oid);  
  oid  | relname   
-------+---------  
 44804 | tmp  
(1 row)  
  
********* QUERY **********  
SELECT c.oid,  
  n.nspname,  
  c.relname  
FROM pg_catalog.pg_class c  
     LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace  
WHERE c.relname ~ '^(tmp)$'  
  AND pg_catalog.pg_table_is_visible(c.oid)  
ORDER BY 2, 3;  
**************************  
  
********* QUERY **********  
SELECT c.relchecks, c.relkind, c.relhasindex, c.relhasrules, c.relhastriggers, c.relrowsecurity, c.relforcerowsecurity, c.relhasoids, pg_catalog.array_to_string(c.reloptions || array(select 'toast.' || x from pg_catalog.unnest(tc.reloptions) x), ', ')  
, c.reltablespace, CASE WHEN c.reloftype = 0 THEN '' ELSE c.reloftype::pg_catalog.regtype::pg_catalog.text END, c.relpersistence, c.relreplident  
FROM pg_catalog.pg_class c  
 LEFT JOIN pg_catalog.pg_class tc ON (c.reltoastrelid = tc.oid)  
WHERE c.oid = '44810';  
**************************  
  
********* QUERY **********  
SELECT a.attname,  
  pg_catalog.format_type(a.atttypid, a.atttypmod),  
  (SELECT substring(pg_catalog.pg_get_expr(d.adbin, d.adrelid) for 128)  
   FROM pg_catalog.pg_attrdef d  
   WHERE d.adrelid = a.attrelid AND d.adnum = a.attnum AND a.atthasdef),  
  a.attnotnull, a.attnum,  
  (SELECT c.collname FROM pg_catalog.pg_collation c, pg_catalog.pg_type t  
   WHERE c.oid = a.attcollation AND t.oid = a.atttypid AND a.attcollation <> t.typcollation) AS attcollation, a.attidentity,  
  NULL AS indexdef,  
  NULL AS attfdwoptions,  
  a.attstorage,  
  CASE WHEN a.attstattarget=-1 THEN NULL ELSE a.attstattarget END AS attstattarget, pg_catalog.col_description(a.attrelid, a.attnum)  
FROM pg_catalog.pg_attribute a  
WHERE a.attrelid = '44810' AND a.attnum > 0 AND NOT a.attisdropped  
ORDER BY a.attnum;  
**************************  
  
********* QUERY **********  
SELECT inhparent::pg_catalog.regclass,          pg_get_expr(c.relpartbound, inhrelid),          pg_get_partition_constraintdef(inhrelid) FROM pg_catalog.pg_class c JOIN pg_catalog.pg_inherits ON c.oid = inhrelid WHERE c.oid = '44810' AND c.relispartition;  
**************************  
  
********* QUERY **********  
SELECT pol.polname, pol.polpermissive,  
CASE WHEN pol.polroles = '{0}' THEN NULL ELSE array_to_string(array(select rolname from pg_roles where oid = any (pol.polroles) order by 1),',') END,  
pg_catalog.pg_get_expr(pol.polqual, pol.polrelid),  
pg_catalog.pg_get_expr(pol.polwithcheck, pol.polrelid),  
CASE pol.polcmd  
WHEN 'r' THEN 'SELECT'  
WHEN 'a' THEN 'INSERT'  
WHEN 'w' THEN 'UPDATE'  
WHEN 'd' THEN 'DELETE'  
END AS cmd  
FROM pg_catalog.pg_policy pol  
WHERE pol.polrelid = '44810' ORDER BY 1;  
**************************  
  
********* QUERY **********  
SELECT oid, stxrelid::pg_catalog.regclass, stxnamespace::pg_catalog.regnamespace AS nsp, stxname,  
  (SELECT pg_catalog.string_agg(pg_catalog.quote_ident(attname),', ')  
   FROM pg_catalog.unnest(stxkeys) s(attnum)  
   JOIN pg_catalog.pg_attribute a ON (stxrelid = a.attrelid AND  
        a.attnum = s.attnum AND NOT attisdropped)) AS columns,  
  (stxkind @> '{d}') AS ndist_enabled,  
  (stxkind @> '{f}') AS deps_enabled  
FROM pg_catalog.pg_statistic_ext stat WHERE stxrelid = '44810'  
ORDER BY 1;  
**************************  
  
********* QUERY **********  
SELECT pub.pubname  
 FROM pg_catalog.pg_publication pub  
 LEFT JOIN pg_catalog.pg_publication_rel pr  
      ON (pr.prpubid = pub.oid)  
WHERE pr.prrelid = '44810' OR pub.puballtables  
ORDER BY 1;  
**************************  
  
********* QUERY **********  
SELECT c.oid::pg_catalog.regclass FROM pg_catalog.pg_class c, pg_catalog.pg_inherits i WHERE c.oid=i.inhparent AND i.inhrelid = '44810' AND c.relkind != 'p' ORDER BY inhseqno;  
**************************  
  
********* QUERY **********  
SELECT c.oid::pg_catalog.regclass, pg_get_expr(c.relpartbound, c.oid) FROM pg_catalog.pg_class c, pg_catalog.pg_inherits i WHERE c.oid=i.inhrelid AND i.inhparent = '44810' AND EXISTS (SELECT 1 FROM pg_class c WHERE c.oid = '44810') ORDER BY c.oid::pg_catalog.regclass::pg_catalog.text;  
**************************  
  
                                   Table "pg_temp_3.tmp"  
 Column |  Type   | Collation | Nullable | Default | Storage  | Stats target | Description   
--------+---------+-----------+----------+---------+----------+--------------+-------------  
 id     | integer |           |          |         | plain    |              |   
 info   | text    |           |          |         | extended |              |   

複原方法

postgres=# create temp table tmp(id int, info text);  
CREATE TABLE  

小結

主備切換時,將會話資源狀態進行平移,可以大幅提升客戶端的體驗,使得數據庫硬件維護、遷移等工作也會變得更加輕鬆。

中間件需要維護客戶端連接和數據庫會話的映射關係,平移後映射關係同樣需要保持一致。

參考

https://www.postgresql.org/docs/10/static/libpq-exec.html

https://www.postgresql.org/docs/10/static/sql-discard.html

最後更新:2017-06-19 19:31:44

  上一篇:go  CentOS環境下tomcat啟動超級慢的解決方案
  下一篇:go  玩過這些經典單機遊戲,就說明你已經老了