萬字長文 | 業內 MySQL 線程池主流方案詳解 - MariaDB/Percona/AliSQL/TXSQL/MySQL企業版

来源:https://www.cnblogs.com/dbkernel/archive/2023/10/29/17795927.html
-Advertisement-
Play Games

作者:盧文雙 資深資料庫內核研發 本文首發於 2023-05-04 22:07:40 http://dbkernel.com/2023/05/04/mysql-threadpool-main-solutions-details/# 本文主要從功能層面對比 percona-server、mariadb ...


作者:盧文雙 資深資料庫內核研發

本文首發於 2023-05-04 22:07:40

http://dbkernel.com/2023/05/04/mysql-threadpool-main-solutions-details/#

本文主要從功能層面對比 percona-server、mariadb、阿裡雲 AliSQL、騰訊 TXSQL、MySQL 企業版線程池方案,都基於 MySQL 8.0。

至於源碼層面,騰訊、阿裡雲、MySQL 企業版不開源,percona 借鑒了 mariadb 早期版本的實現,但考慮到線程池代碼只有 2000 行左右,相對簡單,本文就不做深入闡述。

版本:
MariaDB 10.9,
Percona-Server-8.0.32-24

背景

社區版的 MySQL 的連接處理方法預設是為每個連接創建一個工作線程的one-thread-per-connectionPer_thread)模式。這種模式存在如下弊端:

  • 由於系統的資源是有限的,隨著連接數的增加,資源的競爭也增加,連接的響應時間也隨之增加,如 response time 圖所示。

response time圖

  • 在資源未耗盡時,資料庫整體吞吐隨著連接數增加。一旦連接數超過了某個耗盡系統資源的臨界點,由於各線程互相競爭,CPU 時間片在大量線程間頻繁調度,不同線程上下文頻繁切換,徒增系統開銷,資料庫整體吞吐反而會下降,如下圖所示。

Per_Thread 模式下資料庫整體吞吐

Q:如何避免 在連接數暴增時,因資源競爭而導致系統吞吐下降的問題呢?

MariaDB & Percona 中給出了簡潔的答案:線程池

線程池的原理在percona blog 中有生動的介紹,其大致可類比為早高峰期間大量汽車想通過一座大橋,如果採用one-thread-per-connection的方式則放任汽車自由行駛,由於橋面寬度有限,最終將導致所有汽車寸步難行。線程池的解決方案是限制同時行駛的汽車數,讓橋面時刻保持最大吞吐,儘快讓所有汽車抵達對岸。

資料庫內核月報文章 《MySQL · 最佳實踐 · MySQL 多隊列線程池優化》中舉了一個高鐵買票的例子,也很形象,由於售票員(類比為 CPU 的核數)有限,當有 1000 個用戶(類比為資料庫連接)都想買票時,如果採用 one-thread-per-connection 的方式,則每個人都有一個專用視窗,需要售票員跑來跑去(CPU 上下文切換,售票視窗越多,跑起來越費力)來為你服務,可以看到這是不夠合理的,特別是售票員比較少而購票者很多的場景。如果採用線程池的思想,則不再是每個人都有一個專用的售票視窗(每個客戶端對應一個後端線程),而是通過限定售票視窗數,讓購票者排隊,來減少售票員跑來跑去的成本。

回歸到資料庫本身,MySQL 預設的線程使用模式是會話獨占模式(one-thread-per-connection),每個會話都會創建一個獨占的線程。當有大量的會話存在時,會導致大量的資源競爭,同時,大量的系統線程調度和緩存失效也會導致性能急劇下降

線程池線程池功能旨在解決以上問題,在存在大量連接的場景下,通過線程池實現線程復用

  • 當連接多、併發低時,通過連接復用,避免創建大量空閑線程,減少系統資源開銷。
  • 當連接多、併發高時,通過限制同時運行的線程數,將其控制在合理的範圍內,可避免線程調度工作過多和大量緩存失效,減少線程池間上下文切換和熱鎖爭用,從而對 OLTP 場景產生積極影響。

當連接數上升時,線上程池的幫助下,將資料庫整體吞吐維持在一個較高水準,如圖所示。

線程池模式下資料庫整體吞吐

適用場景

線程池採用一定數量的工作線程來處理連接請求,線程池在查詢相對較短且工作負載受 CPU 限制的情況下效率最高,通常比較適應於 OLTP 工作負載的場景。如果工作負載不受 CPU 限制,那麼您仍然可以通過限制線程數量來為資料庫記憶體緩衝區節省記憶體。

線程池的不足在於當請求偏向於慢查詢時,工作線程阻塞在高時延操作上,難以快速響應新的請求,導致系統吞吐量反而相較於傳統 one-thread-per-connection 模式更低。

線程池適用的場景

  • 對於大量連接的 OLTP 短查詢的場景將有最大收益。
  • 對於大量連接的只讀短查詢也有明顯收益。
  • 有效避免大量連接高併發下資料庫性能衰減。

不太適合用線程池的場景:

  • 具有突發工作負載的場景。在這種場景下,許多用戶往往長時間處於非活躍狀態,但個別時候又處於特別活躍的狀態,同時,對延遲的容忍度較低,因此,線程池節流效果不太理想。不過,即使在這種情況下,也可以通過調整線程的退役頻率來提高性能(使用 thread_pool_idle_timeout 參數)。
  • 高併發、長耗時語句的場景。在這種場景下,併發較多,且都是執行時間較長的語句,會導致工作線程堆積,一旦達到上限,完全阻止後續語句的執行,比如最常見的數據倉庫場景。當然這樣的場景下,不管是否使用線程池,資料庫的表現都是不夠理想的,需要應用側控制大查詢的併發度
  • 有較嚴重的鎖衝突,如果處於鎖等待的工作線程數超過匯流排程數,也會堆積起來,阻止無鎖待的處理請求。比如某個會話執行 FLUSH TABLES WITH READ LOCK語句獲得全局鎖後暫停,那麼其他執行寫操作的客戶端連接就會阻塞,當阻塞的數量超過線程池的上限時,整個 server 都會阻塞。當然這樣的場景下,不管是否使用線程池,資料庫的表現都是不夠理想的,需要應用側進行優化
  • 極高併發的 Prepared Statement 請求。使用 Prepared Statement(Java 應用不算)時,會使用 MySQL Binary Protocol,會增加很多的網路來回操作,比如參數的綁定、結果集的返回,在極高請求壓力下會給 epoll 監聽進程帶來一定的壓力,處於事務狀態中時,可能會讓普通請求得不到執行機會。

為了應對這種阻塞問題,一般會允許配置 extra_portadmin_port 來管理連接。

總結一句話,線程池更適合短連接或短查詢的場景。

行業方案:Percona 線程池實現

由於市面上的線程池方案大多都借鑒了 percona、mariadb 的方案,因此,首先介紹下 percona 線程池的工作機制,再說明其他方案相較於 percona 做了什麼改進。

0. 基本原理

線程池的基本原理為:預先創建一定數量的工作線程(worker 線程)。線上程池監聽線程(listener 線程)從現有連接中監聽到新請求時,從工作線程中分配一個線程來提供服務。工作線程在服務結束之後不銷毀線程(處於 idle 狀態一段時間後會退出),而是保留線上程池中繼續等待下一個請求來臨。

下麵我們將從線程池架構、新連接的創建與分配、listener 線程、worker 線程、timer 線程等幾個方面來介紹 percona 線程池的實現。

1. 線程池的架構

線程池由多個線程組(thread group)timer 線程組成,如下圖所示。

線程組的數量是線程池併發的上限,通常而言線程組的數量需要配置成資料庫實例的 CPU 核心數量(可通過參數thread_pool_size設置),從而充分利用 CPU。線程組之間通過線程ID % 線程組數的方式分配連接,線程組內通過競爭方式處理連接。

線程池中還有一個服務於所有線程組的timer 線程,負責周期性(檢查時間間隔為threadpool_stall_limit毫秒)檢查線程組是否處於阻塞狀態。當檢測到阻塞的線程組時,timer 線程會通過喚醒或創建新的工作線程(wake_or_create_thread 函數)來讓線程組恢復工作。

創建新的工作線程不是每次都能創建成功,要根據當前的線程組中的線程數是否大於線程組中的連接數,活躍線程數是否為 0,以及上一次創建線程的時間間隔是否超過閾值(這個閾值與線程組中的線程數有關,線程組中的線程數越多,時間間隔越大)。

Percona線程池組成部分

線程組內部由多個 worker 線程、0 或 1 個動態 listener 線程、高低優先順序事件隊列(由網路事件 event 構成)、mutex、epollfd、統計信息等組成。如下圖所示:

Percona線程池組架構

worker 線程:主要作用是從隊列中讀取並處理事件。

  • 如果該線程所在組中沒有 listener 線程,則該 worker 線程將成為 listener 線程,通過 epoll 的方式監聽數據,並將監聽到的 event 放到線程組中的隊列。
  • worker 線程數目動態變化,併發較大時會創建更多的 worker 線程,當從隊列中取不到 event 時,work 線程將休眠,超過一定時間後結束線程。
  • 一個 worker 線程只屬於一個線程組。

listener 線程:當高低隊列為空,listen 線程會自己處理,無論這次獲取到多少事務。否則 listen 線程會把請求加入到隊列中,如果此時active_thread_count=0,喚醒一個工作線程

高低優先順序隊列:為了提高性能,將隊列分為優先隊列和普通隊列。這裡採用引入兩個新變數thread_pool_high_prio_ticketsthread_pool_high_prio_mode。由它們控制高優先順序隊列策略。對每個新連接分配可以進入高優先順序隊列的 ticket。

2. 新連接的創建與分配

新連接接入時,線程池按照新連接的線程 id 取模線程組個數來確定新連接歸屬的線程組(thd->thread_id() % group_count)。這樣的分配邏輯非常簡潔,但由於沒有充分考慮連接的負載情況,繁忙的連接可能會恰巧被分配到相同的線程組,從而導致負載不均衡的現象,這是 percona 線程池值得被優化的點

Percona線程池整體架構圖

選定新連接歸屬的線程組後,新連接申請被作為事件放入低優先順序隊列中,等待線程組中 worker 線程將高優先順序事件隊列處理完後,就會處理低優先順序隊列中的請求。

3. listener 線程

listener 線程是負責監聽連接請求的線程,每個線程組都有一個listener 線程

percona 線程池的 listener 採用epoll實現。當 epoll 監聽到請求事件時,listener 會根據請求事件的類型來決定將其放入哪個優先順序事件隊列。將事件放入高優先順序隊列的條件如下(見函數connection_is_high_prio),只需要滿足其一即可

  • 當前線程池的工作模式為高優先順序模式,在此模式下只啟用高優先順序隊列。(mode == TP_HIGH_PRIO_MODE_STATEMENTS
  • 當前線程池的工作模式為高優先順序事務模式,在此模式下每個連接的 event最多被放入高優先順序隊列threadpool_high_prio_tickets次。超過threadpool_high_prio_tickets次後,該連接的請求事件只能被放入低優先順序(mode == TP_HIGH_PRIO_MODE_TRANSACTIONS),同時,也會重置票數。
  • 連接持有表鎖
  • 連接持有mdl 鎖
  • 連接持有全局讀鎖
  • 連接持有backup 鎖
inline bool connection_is_high_prio(const connection_t &c) noexcept {
  const ulong mode = c.thd->variables.threadpool_high_prio_mode;

  return (mode == TP_HIGH_PRIO_MODE_STATEMENTS) ||
         (mode == TP_HIGH_PRIO_MODE_TRANSACTIONS && c.tickets > 0 &&
          (thd_is_transaction_active(c.thd) ||
           c.thd->variables.option_bits & OPTION_TABLE_LOCK ||
           c.thd->locked_tables_mode != LTM_NONE ||
           c.thd->mdl_context.has_locks() ||
           c.thd->global_read_lock.is_acquired() ||
           c.thd->backup_tables_lock.is_acquired() ||
           c.thd->mdl_context.has_locks(MDL_key::USER_LEVEL_LOCK) ||
           c.thd->mdl_context.has_locks(MDL_key::LOCKING_SERVICE)));
}

被放入高優先順序隊列的事件可以優先被 worker 線程處理。

只有當高優先順序隊列為空,並且當前線程組不繁忙的時候才處理低優先順序隊列中的事件。線程組繁忙(too_many_busy_threads)的判斷條件是當前組內活躍工作線程數+組內處於等待狀態的線程數大於線程組工作線程額定值thread_pool_oversubscribe+1)。這樣的設計可能帶來的問題是在高優先順序隊列不為空或者線程組繁忙時低優先順序隊列中的事件遲遲得不到響應,這同樣也是 percona 線程池值得被優化的一個點

listener 線程將事件放入高低優先順序隊列後,如果線程組的活躍 worker 數量為 0,則喚醒或創建新的 worker 線程來處理事件。

percona 的線程池中listener 線程和 worker 線程是可以互相切換的,詳細的切換邏輯會在「worker 線程」一節介紹。

  • epoll 監聽到請求事件時,如果高低優先順序事件隊列都為空,意味著此時線程組非常空閑,大概率不存在活躍的 worker 線程。
  • listener 在此情況下會將除第一個事件外的所有事件按前述規則放入高低優先順序事件隊列,然後退出監聽任務,親自處理第一個事件
  • 這樣設計的好處在於當線程組非常空閑時,可以避免 listener 線程將事件放入隊列,喚醒或創建 worker 線程來處理事件的開銷,提高工作效率。

Percona listener 線程流程圖

上圖來源於騰訊資料庫技術公眾號

4. worker 線程

worker 線程是線程池中真正幹活的線程,正常情況下,每個線程組都會有一個活躍的 worker 線程。

worker 在理想狀態下,可以高效運轉並且快速處理完高低優先順序隊列中的事件。但是在實際場景中,worker 經常會遭遇 IO、鎖等等待情況而難以高效完成任務,此時任憑 worker 線程等待將使得在隊列中的事件遲遲得不到處理,甚至可能出現長時間沒有 listener 線程監聽新請求的情況。為此,每當 worker 遭遇 IO、鎖等等待情況,如果此時線程組中沒有 listener 線程或者高低優先順序事件隊列非空,並且沒有過多活躍 worker,則會嘗試喚醒或者創建一個 worker。

為了避免短時間內創建大量 worker,帶來系統吞吐波動,線程池創建 worker 線程時有一個控制單位時間創建 worker 線程上限的邏輯,線程組內連接數越多則創建下一個線程需要等待的時間越長。

線程組活躍 worker 線程數量大於等於too_many_active_threads+1時,認為線程組的活躍 worker 數量過多。此時需要對 worker 數量進行適當收斂,首先判斷當前線程組是否有 listener 線程:

  • 如果沒有 listener 線程,則將當前 worker 線程轉化為 listener 線程。
  • 如果當前有 listener 線程,則在進入休眠前嘗試通過epoll_wait獲取一個尚未進入隊列的事件,成功獲取到後立刻處理該事件,否則進入休眠等待被喚醒,等待threadpool_idle_timeout時間後仍未被喚醒則銷毀該 worker 線程。

worker 線程與 listener 線程的切換如下圖所示:

Percona worker線程與listener線程的切換

上圖來自於騰訊資料庫技術公眾號

5. timer 線程

timer 線程每隔threadpool_stall_limit時間進行一次所有線程組的掃描(check_stall)。

當線程組高低優先順序隊列中存在事件,並且自上次檢查至今沒有新的事件被 worker 消費,則認為線程組處於停滯狀態。

  • 停滯的主要原因可能是長時間執行的非阻塞請求, 也可能發生於線程正在等待但 wait_begin/wait_end (嘗試喚醒或創建新的 worker 線程)被上層函數忘記調用的場景。
  • timer 線程會通過喚醒或創建新的 worker 線程來讓停滯的線程組恢復工作。

timer 線程為了儘量減少對正常工作的線程組的影響,在check_stall時採用的是try_lock的方式,如果加不上鎖則認為線程組運轉良好,不再去打擾。

timer 線程除上述工作外,還負責終止空閑時間超過wait_timeout秒的客戶端。

下麵是 Percona 的實現:

check_stall 函數:

check_stall
|-- if (!thread_group->listener && !thread_group->io_event_count) {
|--   wake_or_create_thread(thread_group); // 重點函數
|-- }
|-- thread_group->io_event_count = 0; // 表示自上次 check 之後,當前線程組新獲取的 event 數
|-- if (!thread_group->queue_event_count && !queues_are_empty(*thread_group)) { // 重點函數
|--   thread_group->stalled = true;
|--   wake_or_create_thread(thread_group); // 重點函數
|-- }
|-- thread_group->queue_event_count = 0;

static bool queues_are_empty(const thread_group_t &tg) noexcept {
return (tg.high_prio_queue.is_empty() && // 重點函數
(tg.queue.is_empty() || too_many_busy_threads(tg))); // 重點函數
}
  • io_event_count:當 Listen 線程監聽到事件時++
  • queue_event_count :當 work 線程從隊列中獲取事件時++

行業主流方案對比

MySQL 企業版 vs MariaDB

MySQL 企業版是在 5.5 版本引入的線程池,以插件的方式實現的。

相同點:

  • 都具備線程池功能,都支持 thread_pool_size 參數。
  • 都支持專有 listener 線程(thread_pool_dedicated_listeners 參數)。
  • 都支持高低優先順序隊列,且在避免低優先順序隊列事件餓死方面,二者採用了相同方案,即低優先順序隊列事件等待一段時間(thread_pool_prio_kickup_timer 參數)即可移入高優先順序隊列。
  • 都使用相同的機制來探測處於停滯(stall)狀態的線程,都提供了 thread_pool_stall_limit 參數(MariaDB 單位是 ms,MySQL 企業版單位是 10ms)。

不同點: Windows 平臺實現方式不同。

  • MariaDB 使用 Windows 自帶的線程池,而 MySQL 企業版的實現用到了 WSAPoll() 函數(為了便於移植 Unix 程式而提供),因此,MySQL 企業版的實現將不能使用命名管道和共用記憶體。
  • MariaDB 為每個操作系統都使用最高效的 IO 多路復用機制。
    • Windows:原生線程池
    • Linuxepoll
    • Solaris (event ports)
    • FreeBSD and OSX (kevent)
  • 而 MySQL 企業版只在 Linux 上才使用優化過的 IO 多路復用機制 epoll,其他平臺則用 poll

MariaDB vs Percona

Percona 的實現移植自 MariaDB,併在此基礎上添加了一些功能。特別是 Percona 在 5.5-5.7 版本添加了優先順序調度。而 MariaDB 10.2 也支持了優先順序調度,和 Percona 的工作方式類似,只是細節有所不同。

  • MariaDB 10.2 版本的參數 thread_pool_priority=auto,high,low 對應於 Percona 的 thread_pool_high_prio_mode=transactions,statements,none
  • MariaDB 10.2 版本中只有處於事務中的連接才是高優先順序,而 Percona 中符合高優先順序的情況包括:1)處於事務中;2)持有表鎖;3)持有 MDL 鎖;4)持有全局讀鎖;5)持有 backup 鎖
  • 關於避免低優先順序隊列語句餓死的問題:
    • Percona 有一個 thread_pool_high_prio_tickets 參數,用於指定每個連接在高優先順序隊列中的 tickets 數量,而 MariaDB 沒有相應參數。
    • MariaDB 有一個 thread_pool_prio_kickup_timer 參數,可讓低優先隊列中的語句在等待指定時間後移入高優先順序隊列,而 Percona 沒有相應參數。
  • MariaDB 有參數thread_pool_dedicated_listenerthread_pool_exact_stats,而 Percona 沒有。
    • thread_pool_dedicated_listener :可用於指定專有 listener 線程,其只負責epoll_wait等待網路事件,不會變為 worker 線程。預設為 OFF,表示不固定 listener。
    • thread_pool_exact_stats :是否使用高精度時間戳。
  • MariaDB (比如 10.9 版本)在 information_schema 中新增了四張表(THREAD_POOL_GROUPSTHREAD_POOL_QUEUESTHREAD_POOL_STATSTHREAD_POOL_WAITS),便於監控線程池狀態。

AliSQL vs Percona

AliSQL 線程池也一定程度借鑒了 Percona 的機制,但也有自己的特色:

  • AliSQL 線程池給予管理類的 SQL 語句更高的優先順序,保證這些語句優先執行。這樣在系統負載很高時,新建連接、管理、監控等操作也能夠穩定執行
  • AliSQL 線程池給予複雜查詢 SQL 語句相對較低的優先順序,並且有最大併發數的限制。這樣可以避免過多的複雜 SQL 語句將系統資源耗盡,導致整個資料庫服務不可用
  • AliSQL 支持動態開關線程池
  • 從官網手冊及內核月報公開資料,無法獲知 AliSQL 是否支持線程組專職 listener 。

AliSQL 雖然也使用了隊列,但沒有直接採用 percona 或 mariadb 的高低優先順序調度策略,結合官方手冊和資料庫內核月報 2019 年 2 月份的文章 《MySQL 多隊列線程優化》,推測是使用了兩層隊列:

  • 第一層隊列為網路請求隊列,可以區分為請求隊列(不在事務狀態中的請求)和高優先順序隊列(已經在事務狀態中的請求,收到請求後會馬上執行,不進入第二隊列)。
  • 第二層隊列為工作任務隊列,可以區分為查詢隊列、更新隊列和事務隊列

第一層請求隊列的請求經過快速的處理和分析進入第二層隊列。如果是管理操作,則直接執行(假定所有管理操作都是小操作)。

對第二層隊列,可以分別設置一個允許的併發度(可以接近 CPU 的個數),以實現匯流排程數的控制。只要線程數大於四類操作的設計併發度之和,則不同類型的操作不會互相干涉(在這裡是假定同一操作超過各自併發度而進行排隊是合理的)。任何一個隊列超過一定的時間,如果沒有完成任何語句,處於阻塞模式,則可以考慮放行,在 MySQL 線程池中有thread_pool_stall_limit變數來控制這個間隔,以防止任何一個隊列掛起。

可以從配置參數的變化來瞭解優化後的線程池工作機制:

  • thread_pool_enabled :線程池開關。
  • thread_pool_idle_timeout :線程最大空閑時間,超過則退出。
  • thread_pool_max_threads :線程池最大工作線程數。
  • thread_pool_oversubscribe:每個 Thread Group 的目標線程數。
  • thread_pool_normal_weights(相較 percona 新加):查詢、更新操作的目標線程比例(假定這兩類操作的比重相同),即併發度 = thread_pool_oversubscribe * 目標比例/100
  • thread_pool_trans_weights(相較 percona 新加):事務操作的目標線程比例,即併發度 = thread_pool_oversubscribe * 目標比例/100
  • thread_pool_stall_limit:阻塞模式檢查頻率(同時檢查 5 個隊列的狀態)
  • thread_pool_size:線程組的個數(在優化鎖併發後,線程組的個數不是很關鍵,可以用來根據物理機器的資源配置情況來軟性調節處理能力)

另外,AliSQL 新增了 6 個狀態變數:thread_pool_active_threadsthread_pool_big_threadsthread_pool_dml_threadsthread_pool_qry_threadsthread_pool_trx_threadsthread_pool_wait_threads 。還有 2 個狀態變數與 percona 線程池含義相同,只是名字不同。

TXSQL vs Percona

騰訊雲 TXSQL 線程池核心方案與 Percona 完全一樣,額外支持的功能如下:

1. 支持線程池動態切換

線程池採用一定數量的工作線程來處理用戶連接請求,通常比較適應於 OLTP 工作負載的場景。但線程池並不是萬能的,線程池的不足在於當用戶請求偏向於慢查詢時,工作線程阻塞在高時延操作上,難以快速響應新的用戶請求,導致系統吞吐量反而相較於 one-thread-per-connection(簡稱為 Per_thread)模式更低。

Per_thread 模式與 Thread_pool 模式各有優劣,系統需要根據用戶的業務類型靈活切換兩種模式。在業務高峰時段切換模式,重啟伺服器,會嚴重影響用戶業務。為瞭解決此問題,TXSQL 提出了線程池動態切換的優化,即在不重啟資料庫服務的情況下,動態開啟或關閉線程池。

通過參數 thread_handling_switch_mode 控制,可選值及含義如下:

可選值 含義
disabled 禁止模式動態遷移
stable 只有新連接遷移
fast 新連接 + 新請求都遷移,預設模式
sharp kill 當前活躍連接,迫使用戶重連,達到快速切換的效果

在瞭解了 TXSQL 動態線程池的使用方法後,我們再來瞭解一下其具體的實現。

mysql 的thread_handling參數代表了連接管理方法。

在原生 mysql 中,thread_handling 是只讀參數,不允許線上修改

thread_handling 參數對應的底層實現對象是Connection_handler_manager,後者是 mysql 提供連接管理服務的單例類,可對外提供多種連接管理服務:

  • Per_thread : 參數值是 one-thread-per-connection
  • No_threads : 參數值是 no-threads
  • Thread_pool : 新加
  • Plugin_connection_handler : 參數值是 loaded-dynamically

在 mysql 啟動時Connection_handler_manager只需要按照thread_handling初始化一種連接管理方法即可。

為了支持動態線程池,允許用戶連接從 Per_thread 和 Thread_pool 模式中來回切換,我們需要允許多種連接管理方法同時存在。因此,在 mysql 初始化階段,TXSQL 初始化了所有連接管理方法

在支持thread_handlingPer_thread 和 Thread_pool 模式中來回切換後,我們需要考慮的問題主要有以下幾個:

1.1. 活躍用戶連接的 thread_handling 切換

Per_thread 模式下,每個用戶連接對應一個handle_connection線程,handle_connection線程既負責用戶網路請求的監聽,又負責請求的處理。

Thread_pool 模式下,每個 thread_group 都用epoll來管理其中所有用戶連接的網路事件,監聽到的事件放入事件隊列中,交予 worker 處理。

不論是哪種模式,在處理請求的過程中(do_command)切換都不是一個好選擇,而在完成一次 command 之後,尚未接到下一次請求之前是一個較合適的切換點。

  • 為實現用戶連接從 Per_thread 到 Thread_pool 的切換,需要在請求處理完(do_command)之後判斷thread_handling是否發生了變化。
    如需切換則立刻按照 2.2 中介紹的邏輯,通過thread_id % group_size選定目標 thread_group,將當前用戶連接遷移至 Thread_pool 的目標 thread_group 中,後續該用戶連接的所有網路事件統一交予 thread_group 的 epoll 監聽。在完成連接遷移之後,handle_connection 線程即可完成退出或者緩存至下一次 Per_thread 模式處理新連接時復用(此為原生 mysql 支持的邏輯,目的是避免 Per_thread 模式下頻繁地創建和銷毀 handle_connection 線程)。
    • handle_connection 函數被 Per_thread_connection_handler::add_connection 函數調用。
  • 為實現用戶連接從 Thread_pool 到 Per_thread 的切換,需要在請求處理完(threadpool_process_request)後,將用戶線程網路句柄重新掛載到 epollstart_io)之前判斷 thread_handling 是否發生了變化。如需切換則先將網路句柄從 epoll 中移除以及將連接的信息從對應 thread_group 中清除。由於 Per_thread 模式下每個連接對應一個 handle_connection 線程,還需為當前用戶連接創建一個 handle_connection 線程,後續當前用戶連接的網路監聽和請求處理都交予該 handle_connection 線程處理。

1.2. 新連接的處理

由於 thread_handling 可能隨時動態變化,為了使得新連接能被新 thread_handling 處理,需要在新連接處理介面Connection_handler_manager::process_new_connection中,讀取最新的 thread_handling,利用其相應的連接管理方法添加新連接

  • 對於 Per_thread 模式,需要為新連接創建handle_connection線程;
  • 對於 Thread_pool 模式,則需要為新連接選定 thread_group 和將其網路句柄綁定到 thread_group 的epoll中。

1.3. thread_handling 切換的快速生效

從前文的討論中可以看到,處於連接狀態的用戶線程需要等到一個請求處理結束才會等到合適的切換點

如果該用戶連接遲遲不發送網路請求,則連接會阻塞在 do_command 下的get_command的網路等待中,無法及時切換到 Thread_pool。如何快速完成此類線程的切換呢?

一種比較激進的方法就是迫使此類連接重連,在重連後作為新連接自然地切換到 Thread_pool 中,其下一個網路請求也將被 Thread_pool 應答。

線程池動態切換對性能的影響

  • pool-of-threads 切換為 one-thread-per-connection 過程本身不會帶來 query 堆積,以及性能影響
  • one-thread-per-connection 切換為 pool-of-threads 過程,由於之前線程池處於休眠狀態,在 QPS 極高並且有持續高壓的情況下,可能存在一定的請求累積。解決方案如下:
    • 方案 1:適當增大 thread_pool_oversubscribe,並適當調小 thread_pool_stall_limit,快速激活線程池。待消化完堆積 SQL 再視情況還原上述修改。
    • 方案 2:出現 SQL 累積時,短暫暫停或降低業務流量幾秒鐘,等待 pool-of-threads 完成激活,再恢復持續高壓業務流量。

2. 線程池負載均衡優化

如前文所述,新連接按照線程 id 取模線程組個數來確定新連接歸屬的線程組(thd->thread_id() % group_count)。這樣的分配方式未能將各線程組的實際負載考慮在內,因此可能將繁忙的連接分配到相同的線程組,使得線程池出現負載不均衡的現象。為了避免負載不均衡的發生,TXSQL 提出了線程池負載均衡優化。

2.1. 負載的度量

在提出負載均衡的演算法之前,我們首先需要找到一種度量線程組負載狀態的方法,通常我們稱之為"信息策略“。下麵我們分別討論幾種可能的信息策略。

1) queue_length

queue_length代表線程組中低優先順序隊列和高優先順序隊列的長度。此信息策略的最大優勢在於簡單,直接用在工作隊列中尚未處理的 event 的數量描述當前線程組的工作負載情況。此信息策略的不足是 無法將每個網路事件 event 的處理效率納入考量。由於每個 event 的處理效率並不相同,簡單地以工作隊列長度作為度量標準會帶來一些誤判。

2) average_wait_usecs_in_queue

average_wait_usecs_in_queue表示最近 n 個 event 在隊列中的平均等待時間。此信息策略的優勢在於能夠直觀地反映線程組處理 event 的響應速度。某線程組average_wait_usecs_in_queue明顯高於其他線程組說明其工作隊列中的 event 無法及時被處理,需要其他線程組對其提供幫助。

3) group_efficiency

group_efficiency表示一定的時間周期內,線程組處理完的 event 總數占(工作隊列存量 event 數+新增 event 數)的比例。此信息策略的優勢在於能夠直觀反映出線程組一定時間周期內的工作效率,不足在於對於運轉良好的線程組也可能存在誤判:當時間周期選擇不合適時,運轉良好的線程組可能存在時而 group_efficiency 小於 1,時而大於 1 的情況。
上述三種信息策略只是舉例說明,還有更多信息策略可以被採用,就不再一一羅列。

2 .2. 負載均衡的實現介紹

在明確了度量線程組負載的方法之後,我們接下來討論如何均衡負載。我們需要考慮的問題主要如下:

1) 負載均衡演算法的觸發條件

負載均衡操作會將用戶連接從一個線程組遷移至另一個線程組,在非必要情況下觸發用戶連接的遷移反而會導致用戶連接的性能抖動。為儘可能避免負載均衡演算法錯誤觸發,我們需要為觸發負載均衡演算法設定一個負載閾值 M,以及負載比例 N。只有線程組的負載閾值大於 M,並且其與參與均衡負載的線程組的負載比例大於 N 時,才需要啟動負載均衡演算法平衡負載

2) 負載均衡的參數對象

Q:當線程組觸發了負載均衡演算法後,該由哪些線程組參與平衡高負載線程組的負載呢?

很容易想到的一個方案是我們維護全局的線程組負載動態序列,讓負載最輕的線程組負責分擔負載。但是遺憾的是為了維護全局線程組負載動態序列,線程組每處理完一次任務都可能需要更新自身的狀態,併在全局鎖的保護下更新其在全局負載序列中的位置,如此一來對性能的影響勢必較大,因此全局線程組負載動態序列的方案並不理想

為了避免均衡負載對線程池整體性能的影響,需改全局負載比較為局部負載比較。一種可能的方法為噹噹前線程組的負載高於閾值 M 時,只比較其與左右相鄰的 X 個(通常 1-2 個)線程組的負載差異,噹噹前線程組的負載與相鄰線程組的比例也高於 N 倍時,從當前線程組向低負載線程組遷移用戶連接。需要註意的是噹噹前線程組的負載與相鄰線程組的比例不足 N 倍時,說明要麼當前線程組還不夠繁忙、要麼其相鄰線程組也較為忙碌,此時為了避免線程池整體表現惡化,不適合強行均衡負載。

3) 均衡負載的方法

討論完負載均衡的觸發條件及參與對象之後,接下來我們需要討論高負載線程組向低負載線程組遷移負載的方法。總體而言,包括兩種方法:新連接的優化分配、舊連接的合理轉移

在掌握了線程組的量化負載之後,較容易實現的均衡負載方法是在新連接分配線程組時特意避開高負載線程組,這樣一來已經處於高負載狀態的線程組便不會因新連接的加入進一步惡化。但僅僅如此還不夠,如果高負載線程組的響應已經很遲鈍,我們還需要主動將其中的舊連接遷移至合適的低負載線程組,具體遷移時機在 3.1 中已有述及,為在請求處理完(threadpool_process_request)後,將用戶線程網路句柄重新掛載到epoll(start_io)之前,此處便不再展開討論。

3. 線程池斷連優化

3.1. percona 線程池問題

如前文所述,線程池採用 epoll 來處理網路事件。當 epoll 監聽到網路事件時,listener 會將網路事件放入事件隊列或自己處理,此時相應用戶連接不會被 epoll 監聽。percona 線程池需要等到請求處理結束之後才會使用 epoll 重新監聽用戶連接的新網路事件。percona 線程池這樣的設計通常不會帶來問題,因為用戶連接在請求未被處理時,也不會有發送新請求的需求。但特殊情況下,如果用戶連接在重新被 epoll 監聽前自行退出了,此時用戶連接發出的斷連信號無法被 epoll 捕捉,因此在 mysql 伺服器端無法及時退出該用戶連接。這樣帶來的影響主要有兩點:

  1. 用戶連接客戶端雖已退出,但 mysql 伺服器端卻仍在運行該連接,繼續消耗 CPU、記憶體資源,甚至可能繼續持有鎖,只有等到連接超時才能退出;
  2. 由於用戶連接在 mysql 伺服器端未及時退出,連接數也並未清理,如果用戶業務連接數較多,可能導致用戶新連接數觸達最大連接數上限,用戶無法連接資料庫,嚴重影響業務。

為解決上述問題,TXSQL 提出了線程池斷連優化。

3.2. 斷連優化的實現介紹

斷連優化的重點在於及時監聽用戶連接的斷連事件並及時處理。為此需要作出的優化如下:

  1. 在 epoll 接到用戶連接的正常網路事件後,立刻監聽該用戶連接的斷連事件
  2. 所有用戶連接退出從同步改為非同步,所有退出的連接先放入quit_connection_queue,後統一處理;
  3. 一旦 epoll 接到斷連事件後立刻將用戶連接 thd->killed 設置為`THD::KILL_CONNECTION 狀態,並將連接放入 quit_connection_queue 中非同步退出
  4. listener 每隔固定時間(例如 100ms)處理一次 quit_connection_queue,讓其中的用戶連接退出

4. 新增用於監控的狀態變數

  • 新增指令 show threadpool status ,可展示 25 個線程池狀態變數。
  • show full processlist 中新增如下狀態變數:
    • Moved_to_per_thread 表示該連接遷移到 Per_thread 的次數。
    • Moved_to_thread_pool 表示該連接遷移到 Thread_pool 的次數。

性能結果

由於騰訊 TXSQL、Percona 官方手冊都沒有性能數據,因此僅列出其他幾種方案的性能結果。

MariaDB 5.5 - 無優先順序隊列

本小節內容來源於官網手冊

MariaDB 官網是基於 5.5 版本線程池測試的,也就是不支持高低優先順序隊列的版本。

採用 Sysbench 0.4,以pitbull (Linux, 24 cores) 的情況來說明在不同場景下的 QPS 情況。

OLTP_RO

併發數 16 32 64 128 256 512 1024 2048 4096
per_thread 6754 7905 8152 7948 7924 7587 5313 3827 208
threadpool 6566 7725 8108 8079 7976 7793 7429 6523 4456

MariaDB OLTP_RO

OLTP_RW

併發數 16 32 64 128 256 512 1024 2048 4096
per_thread 4561 5316 5332 3512 2874 2476 1380 265 53
threadpool 4504 5382 5694 5567 5302 4514 2548 1186 484

MariaDB OLTP_RW

POINT_SELECT

併發數 16 32 64 128 256 512 1024 2048 4096
per_thread 148673 161547 169747 172083 69036 42041 21775 4368 282
threadpool 143222 167069 167270 165977 164983 158410 148690 147107 143934

MariaDB POINT_SELECT

UPDATE_NOKEY

併發數 16 32 64 128 256 512 1024 2048 4096
per_thread 65213 71680 19418 13008 11155 8742 5645 635 332
threadpool 64902 70236 70037 68926 69930 69929 67099 62376 17766

MariaDB UPDATE_NOKEY

AliSQL

如下是開啟線程池和不開啟線程池的性能對比。從測試結果可以看出線程池在高併發的情況下有著明顯的性能優勢。

update_non_index

AliSQL update_non_index

write_only

AliSQL write_only

read_write

AliSQL read_write

point_select

AliSQL point_select

總結

功能區別

MySQL 企業版 MariaDB Percona 騰訊 TXSQL 阿裡雲 AliSQL
功能實現方式 插件 非插件 非插件 非插件 推測是非插件
版本 5.5 版本引入 5.5 版本引入,10.2 版本完善 5.5-5.7/8.0 5.7/8.0 5.6/5.7/8.0
是否開源
動態開關線程池 插件式,不支持 不支持 不支持 支持 支持
優先順序處理策略 設定高低優先順序,且低優先順序事件等待一段時間可升為高優先順序隊列 設定高低優先順序,且低優先順序事件等待一段時間可升為高優先順序隊列 設定高低優先順序,且限制每個連接在高優先順序隊列中的票數 設定高低優先順序,且限制每個連接在高優先順序隊列中的票數 控制事務、非事務語句的比例
各線程組之間負載均衡優化 不支持 不支持 不支持 支持 -
線程池斷連優化 - 不支持 不支持 支持 -
監控 - 2 個狀態變數 2 個狀態變數 27 個狀態變數 8 個狀態變數
借鑒方案 - - MariaDB Percona MariaDB 5.5
跨平臺 Windows/Unix Windows/Unix/MacOS Windows/Unix - -

Q:如果線程池阻塞了,怎麼處理?

MySQL 8.0.14 以前的版本使用 extra_port 功能(percona & mariadb),8.0.14 及之後版本官方支持了 admin_port 功能。

參數區別

由於業內線程池方案基本都會參考 MariaDB 或 Percona,因此,以 Percona 和 MariaDB 的參數為準,基於 MySQL 8.0,總結其他方案是否有相同或類似參數。

註意:MySQL 企業版核心方案與 MariaDB 類似,且關於差異點,官方描述較少,因此,不做對比。

MariaDB Percona 騰訊 TXSQL 阿裡雲 AliSQL
thread_handling 線程池開關 有類似參數 thread_handling_switch_mode (支持動態開關) 有類似參數 thread_pool_enabled(支持動態開關)
thread_pool_idle_timeout 線程最大空閑時間,超過則退出。
thread_pool_high_prio_mode 高優先順序隊列調度策略,支持 transactions,statements,none 三種策略 有類似參數 thread_pool_priority,支持 high, low, auto 三種策略
thread_pool_high_prio_tickets 控制每個連接在高優先順序中的票數,僅在調度模式是事務模式時生效
thread_pool_max_threads 線程池最大工作線程數
thread_pool_oversubscribe 每個線程組中的最大工作線程數
thread_pool_size 線程組數,一般推薦設為 CPU 核心數
thread_pool_stall_limit timer 線程判斷線程組是否停滯(定期調用 check_stall )的時間間隔
thread_pool_prio_kickup_timer 低優先隊列中的語句在等待該值指定的時間後,則移入高優先順序隊列
thread_pool_dedicated_listener 是否啟用專用 listener 線程。若關閉,則 listener 有可能變為 worker。
thread_pool_exact_stats 是否使用高精度時間戳
thread_pool_normal_weights:查詢、更新操作的目標線程比例(假定這兩類操作的比重相同),即併發度= thread_pool_oversubscribe * 目標比例/100
thread_pool_trans_weights:事務操作的目標線程比例,即併發度= thread_pool_oversubscribe * 目標比例/100

可見

  1. 阿裡雲 AliSQL 線程池資料較少,雖然有些參數不具備,但並不說明未實現對應機制,比如專用 listener 線程。

監控區別

Percona、MariaDB:

只有兩個狀態變數:

  • Threadpool_threads
  • Threadpool_idle_threads

阿裡雲 AliSQL

新增了一些狀態變數:

狀態名 狀態說明
thread_pool_active_threads 線程池中的活躍線程數
thread_pool_big_threads 線程池中正在執行複雜查詢的線程數。複雜查詢包括有子查詢、聚合函數、group by、limit 等的查詢語句。
thread_pool_dml_threads 線程池中的在執行 DML 的線程數
thread_pool_idle_threads 線程池中的空閑線程數
thread_pool_qry_threads 線程池中正在執行簡單查詢的線程數
thread_pool_total_threads 線程池中的匯流排程數
thread_pool_trx_threads 線程池中正在執行事務的線程數
thread_pool_wait_threads 線程池中正在等待磁碟 IO、事務提交的線程數

騰訊雲 TXSQL

新增 show threadpool status 指令,展示的相關狀態如下:

狀態名 狀態說明
groupid 線程組 id
connection_count 線程組用戶連接數
thread_count 線程組內工作線程數
havelistener 線程組當前是否存在 listener
active_thread_count 線程組內活躍 worker 數量
waiting_thread_count 線程組內等待中的 worker 數量(調用 wait_begin 的 worker)
waiting_threads_size 線程組中無網路事件需要處理,進入休眠期等待被喚醒的 worker 數量(等待 thread_pool_idle_timeout 秒後自動銷毀)
queue_size 線程組普通優先順序隊列長度
high_prio_queue_size 線程組高優先順序隊列長度
get_high_prio_queue_num 線程組內事件從高優先順序隊列被取走的總次數
get_normal_queue_num 線程組內事件從普通優先順序隊列被取走的總次數
create_thread_num 線程組內創建的 worker 線程總數
wake_thread_num 線程組內從 waiting_threads 隊列中喚醒的 worker 總數
oversubscribed_num 線程組內 worker 發現當前線程組處於 oversubscribed 狀態,並且準備進入休眠的次數
mysql_cond_timedwait_num 線程組內 worker 進入 waiting_threads 隊列的總次數
check_stall_nolistener 線程組被 timer 線程 check_stall 檢查中發現沒有 listener 的總次數
check_stall_stall 線程組被 timer 線程 check_stall 檢查中被判定為 stall 狀態的總次數
max_req_latency_us 線程組中用戶連接在隊列等待的最長時間(單位毫秒)
conns_timeout_killed 線程組中用戶連接因客戶端無新消息時間超過閾值(net_wait_timeout)被 killed 的總次數
connections_moved_in 從其他線程組中遷入該線程組的連接總數
connections_moved_out 從該線程組遷出到其他線程組的連接總數
connections_moved_from_per_thread 從 one-thread-per-connection 模式中遷入該線程組的連接總數
connections_moved_to_per_thread 從該線程組中遷出到 one-thread-per-connection 模式的連接總數
events_consumed 線程組處理過的 events 總數
average_wait_usecs_in_queue 線程組內所有 events 在隊列中的平均等待時間

show full processlist 中新增如下狀態:

  • Moved_to_per_thread 表示該連接遷移到 Per_thread 的次數。
  • Moved_to_thread_pool 表示該連接遷移到 Thread_pool 的次數.

參考鏈接

  1. 騰訊 TXSQL:
    1. 原創|線程池詳解 (qq.com)
    2. 雲資料庫 MySQL 動態線程池-自研內核 TXSQL-文檔中心-騰訊雲 (tencent.com)
  2. Percona:
    1. Thread pool - Percona Server for MySQL
    2. SimCity outages, traffic control and Thread Pool for MySQL (percona.com)
    3. 關於 MySQL 線程池,這也許是目前最全面的實用帖 - MySQL - dbaplus 社群
  3. MariaDB:
    1. Thread Pool in MariaDB - MariaDB Knowledge Base
  4. 阿裡雲 AliSQL:
    1. MySQL · 特性分析 · 線程池 (taobao.org)
    2. MySQL · 最佳實踐 · MySQL 多隊列線程池優化 (taobao.org)
  5. MySQL 企業版:
    1. MySQL :: MySQL 8.0 Reference Manual :: 5.6.3 MySQL Enterprise Thread Pool

歡迎關註我的微信公眾號【資料庫內核】:分享主流開源資料庫和存儲引擎相關技術。

歡迎關註公眾號資料庫內核
標題 網址
GitHub https://dbkernel.github.io
知乎 https://www.zhihu.com/people/dbkernel/posts
思否(SegmentFault) https://segmentfault.com/u/dbkernel
掘金 https://juejin.im/user/5e9d3ed251882538083fed1f/posts
CSDN https://blog.csdn.net/dbkernel
博客園(cnblogs) https://www.cnblogs.com/dbkernel
莫聽竹林打葉聲,何妨吟嘯且前行。竹杖芒鞋輕勝馬,誰怕?一蓑煙雨任平生。
您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 這是一篇入職3周.NET 實習開發的感悟 吐槽 開篇先吐槽一下吧! 首先吐槽的就是加班費的問題,公司加班費挺多,但是我是試用期(3個月)沒有加班費,但是公司帶我的組長特喜歡加班,老是問我加不加班,懂點人情的都會一起加班,雖然公司規定17.30下班,但是基本都是18.00才開始走人,然後加班一般都是8 ...
  • 在進行2D游戲開發時,跳躍是不可缺少的一個重要功能。但是我們在Unity開發時Unity本身的物理引擎並不能提供很好的的手感,下落的時候輕飄飄的,這操作起來顯然非常不舒服。所以,我們需要自己對跳躍進行優化,以此來獲得更好的手感。我們不難發現,在絕大多數2D游戲的跳躍中,下落的速度比上升的速度要快上很 ...
  • 一、背景: 微軟的.net core開發工具,目前來看,winform界面軟體還沒有打算要支持linux系統下運行的意思,要想讓c#桌面軟體在linux系統上運行,開發起來還比較麻煩。微軟只讓c#的控制台軟體支持在linux運行。 二、解決方案: 我想到的一個方案是自定義封裝軟體的System.Wi ...
  • bind介紹 在區域網環境中,一般我們要搭建DNS服務,使用的是BIND(Berkeley Internet Name Domain)軟體來實現,BIND提供了一個名為named(也叫named daemon)的服務程式,用於處理DNS查詢。 BIND 由 Internet Systems Cons ...
  • 1、當使用makefile自動推導的功能時編譯器報錯ccJS7JEh.s: Assembler messages: ccJS7JEh.s:5: Error: invalid instruction suffix for `push' ccJS7JEh.s:7: Error: invalid inst ...
  • 簡單來說就是一個文件傳遞的機制,首先創建/安裝一個硬碟,然後把前硬碟中的一部分文件先轉移到Linux系統上,再通過Linux系統轉移到創建的新硬碟,之後用虛擬機,把新硬碟裝在其中,就可以在新硬碟上做到一些功能了 前知識準備: Linux啟動流程: 1、首先Linux要通過自檢,檢查硬體設備有沒有故障 ...
  • STM32F3系列 ADC 單端採樣(基於LL庫) 晶元型號:STM32f303RBT6 開發軟體:MDK5 & CubeMX & VS Code 目錄 目錄STM32F3系列 ADC 單端採樣(基於LL庫)目錄引言1 基礎知識1.1ADC轉換基本流程1.2 時鐘樹1.3 關鍵參數1.3.1 位數1 ...
  • 1. Gremlin Server只將數據存儲在記憶體中 1.1. 如果停止Gremlin Server,將丟失資料庫里的所有數據 2. 概念 2.1. 遍歷(動詞) 2.1.1. 當在圖資料庫中導航時,從頂點到邊或從邊到頂點的移動過程 2.1.2. 類似於在關係資料庫中的查詢行為 2.2. 遍歷(名 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...