100Wqps短鏈系統,怎麼設計?

這段時間,在整理知識星球中面試專欄時看到這麼一個字節跳動的二面真題:100Wqps短鏈系統,怎麼設計?

這道題,看上去業務簡單,其實,覆蓋的知識點非常多:

  • 高併發、高性能分佈式 ID
  • Redis Bloom Filter 高併發、低內存損耗的 過濾組件知識
  • 分庫、分表海量數據存儲
  • 多級緩存的知識
  • HTTP傳輸知識
  • 二進制、十六進制、六十二進制知識

總體來說,高併發、高性能系統的核心領域,都覆蓋了。所以,陳某分析下來,得到一個結論:是一個超級好的問題。

1、短URL系統的背景

短網址替代長URL,在互聯網網上傳播和引用。

例如QQ微博的url.cn,新郎的sinaurl.cn等。

在QQ、微博上發佈網址的時候,會自動判別網址,並將其轉換,例如:url.cn/2hytQx

爲什麼要這樣做的,無外乎幾點:

  1. 縮短地址長度,留足更多空間的給 有意義的內容

    URL是沒有意義的,有的原始URL很長,佔用有效的屏幕空間。

    微博限制字數爲140字一條,那麼如果這個連接非常的長,以至於將近要佔用我們內容的一半篇幅,這肯定是不能被允許的,鏈接變短,對於有長度限制的平臺發文,可編輯的文字就變多了, 所以短網址應運而生了。

  2. 可以很好的對原始URL內容管控。

    有一部分網址可以會涵蓋XX,暴力,廣告等信息,這樣我們可以通過用戶的舉報,完全管理這個連接將不出現在我們的應用中,應爲同樣的URL通過加密算法之後,得到的地址是一樣的。

  3. 可以很好的對原始URL進行行爲分析

    我們可以對一系列的網址進行流量,點擊等統計,挖掘出大多數用戶的關注點,這樣有利於我們對項目的後續工作更好的作出決策。

  4. 短網址和短ID相當於間接提高了帶寬的利用率、節約成本

  5. 鏈接太長在有些平臺上無法自動識別爲超鏈接

  6. 短鏈接更加簡潔好看且安全,不暴露訪問參數。而且,能規避關鍵詞、域名屏蔽等手段

2、短URL系統的原理

短URL系統的核心:將長的 URL 轉化成短的 URL

客戶端在訪問系統時,短URL的工作流程如下:

  • 先使用短地址A訪問 短鏈Java 服務
  • 短鏈Java 服務 進行 地址轉換和映射,將 短URL系統映射到對應的長地址URL
  • 短鏈Java 服務 返回302 重定向 給客戶端
  • 然後客戶端再重定向到原始服務

如下圖所示:

那麼,原始URL如何變短呢?簡單來說, 可以將原始的地址,使用編號進行替代

編號如何進一步變短呢? 可以使用更大的進制來表示

六十二進制表示法

顧名思義短網址就是非常短的網址,比如xxx.cn/EYyCO9T,其中核… EYyCO9T 只有7位長度。

其實這裏的7位長度是使用62進制來表示的,就是常用的0-9、a-z、A-Z,也就是10個數字+26個小寫+26個大寫=62位。

那麼7位長度62進制可以表示多大範圍呢?

62^7 = 3,521,614,606,208 (合計3.5萬億),

說明:

10進制 最大隻能生成 10 ^ 6 - 1 =999999個
16進制 最大隻能生成 16 ^ 6 - 1 =16777215個
16進制裏面已經包含了 A B C D E F 這幾個字母
62進制 最大竟能生成 62 ^ 6 - 1 =56800235583個 基本上夠了。
A-Z a-z 0-9 剛好等於62位

注意:

int(4個字節) ,存儲的範圍是-21億到21億
long(8個字節),存儲的範圍是-900萬萬億 到 900萬萬億

至於短網址的長度,可以根據自己需要來調整,如果需要更多,可以增加位數,

即使6位長度62^6也能達到568億的範圍,

這樣的話只要算法得當,可以覆蓋很大的數據範圍。

在編碼的過程中,可以按照自己的需求來調整62進制各位代表的含義。

一個典型的場景是, 在編碼的過程中,如果不想讓人明確知道轉換前是什麼,可以進行弱加密,

比如A站點將字母c表示32、B站點將字母c表示60,就相當於密碼本了。

128進製表示法

標準ASCII 碼也叫基礎ASCII碼,使用7 位二進制數(剩下的1位二進制爲0),包含128個字符,

看到這裏你或許會說,使用128進制(如果有的話)豈不是網址更短,

是的,

7 位二進制數(剩下的1位二進制爲0)表示所有的大寫和小寫字母,數字0 到9、標點符號,以及在美式英語中使用的特殊控制字符 [1] 。

注意:

128個進制就可能會出現大量的不常用字符

比如 # % & * 這些,

這樣的話,對於短鏈接而言,通用性和記憶性就變差了,

所以,62進制是個權衡折中。

3、短 URL 系統的功能分析

假設短地址長度爲8位,62的8次方足夠一般系統使用了

系統核心實現,包含三個大的功能

  • 發號
  • 存儲
  • 映射

可以分爲兩個模塊:發號與存儲模塊、映射模塊

發號與存儲模塊

  • 發號:使用發號器發號 , 爲每個長地址分配一個號碼ID,並且需要防止地址二義,也就是防止同一個長址多次請求得到的短址不一樣
  • 存儲:將號碼與長地址存放在DB中,將號碼轉化成62進制,用於表示最終的短地址,並返回給用戶

映射模塊

用戶使用62進制的短地址請求服務 ,

  • 轉換:將62進制的數轉化成10進制,因爲咱們系統內部是long 類型的10進制的數字ID
  • 映射:在DB中尋找對應的長地址
  • 通過302重定向,將用戶請求重定向到對應的地址上

4、發號器的高併發架構

回顧一下發號器的功能:

  • 爲每個長地址分配一個號碼ID
  • 並且需要防止地址歧義

以下對目前流行的分佈式ID方案做簡單介紹

方案1:使用地址的hash 編碼作爲ID

可以通過 原始Url的 hash編碼,得到一個 整數,作爲 短鏈的ID

哈希算法簡單來說就是將一個元素映射成另一個元素,

哈希算法可以簡單分類兩類,

  • 加密哈希,如MD5,SHA256等,
  • 非加密哈希,如MurMurHash,CRC32,DJB等。

MD5算法

MD5消息摘要算法(MD5 Message-Digest Algorithm),一種被廣泛使用的密碼散列函數,

可以產生出一個128位(16字節)的散列值(hash value),

MD5算法將數據(如一段文字)運算變爲另一固定長度值,是散列算法的基礎原理。

由美國密碼學家 Ronald Linn Rivest設計,於1992年公開並在 RFC 1321 中被加以規範。

CRC算法

循環冗餘校驗(Cyclic Redundancy Check)是一種根據網絡數據包或電腦文件等數據,

產生簡短固定位數校驗碼的一種散列函數,由 W. Wesley Peterson 於1961年發表。

生成的數字在傳輸或者存儲之前計算出來並且附加到數據後面,然後接收方進行檢驗確定數據是否發生變化。

由於本函數易於用二進制的電腦硬件使用、容易進行數學分析並且尤其善於檢測傳輸通道干擾引起的錯誤,因此獲得廣泛應用。

MurmurHash

MurmurHash 是一種非加密型哈希函數,適用於一般的哈希檢索操作。

由 Austin Appleby 在2008年發明,並出現了多個變種,與其它流行的哈希函數相比,對於規律性較強的鍵,MurmurHash的隨機分佈特徵表現更良好。

這個算法已經被很多開源項目使用,比如libstdc++ (4.6版)、Perl、nginx (不早於1.0.1版)、Rubinius、 libmemcached、maatkit、Hadoop、Redis,Memcached,Cassandra,HBase,Lucene等。

MurmurHash 計算可以是 128位、64位、32位,位數越多,碰撞概率越少。

所以,可以把長鏈做 MurmurHash 計算,可以得到的一個整數哈希值 ,

所得到的短鏈,類似於下面的形式

固定短鏈域名+哈希值 = www.weibo.com/888888888

如何縮短域名?傳輸的時候,可以把 MurmurHash之後的數字爲10進制,可以把數字轉成62進制

www.weibo.com/abcdef

那麼,使用地址的hash 編碼作爲ID的問題是啥呢?

會出現碰撞,所以這種方案不適合。

方案2:數據庫自增長ID

屬於完全依賴數據源的方式,所有的ID存儲在數據庫裏,是最常用的ID生成辦法,在單體應用時期得到了最廣泛的使用,建立數據表時利用數據庫自帶的auto_increment作主鍵,或是使用序列完成其他場景的一些自增長ID的需求。

但是這種方式存在在高併發情況下性能問題,要解決該問題,可以通過批量發號來解決,

提前爲每臺機器發放一個ID區間 [low,high],然後由機器在自己內存中使用 AtomicLong 原子類去保證自增,減少對DB的依賴,

每臺機器,等到自己的區間即將滿了,再向 DB 請求下一個區段的號碼,

爲了實現寫入的高併發,可以引入 隊列緩衝+批量寫入架構,

等區間滿了,再一次性將記錄保存到DB中,並且異步進行獲取和寫入操作, 保證服務的持續高併發。

比如可以每次從數據庫獲取10000個號碼,然後在內存中進行發放,當剩餘的號碼不足1000時,重新向MySQL請求下10000個號碼,在上一批號碼發放完了之後,批量進行寫入數據庫。

但是這種方案,更適合於單體的 DB 場景,在分佈式DB場景下, 使用 MySQL的自增主鍵, 會存在不同DB庫之間的ID衝突,又要使用各種辦法去解決,

總結一下, MySQL的自增主鍵生成ID的優缺點和使用場景:

  • 優點:

    非常簡單,有序遞增,方便分頁和排序。

  • 缺點:

    分庫分表後,同一數據表的自增ID容易重複,無法直接使用(可以設置步長,但侷限性很明顯);

    性能吞吐量整個較低,如果設計一個單獨的數據庫來實現 分佈式應用的數據唯一性,

    即使使用預生成方案,也會因爲事務鎖的問題,高併發場景容易出現單點瓶頸。

  • 適用場景:

    單數據庫實例的表ID(包含主從同步場景),部分按天計數的流水號等;

    分庫分表場景、全系統唯一性ID場景不適用。

所以,高併發場景, MySQL的自增主鍵,很少用。

方案3:分佈式、高性能的中間件生成ID

Mysql 不行,可以考慮分佈式、高性能的中間件完成。

比如 Redis、MongoDB 的自增主鍵,或者其他 分佈式存儲的自增主鍵,但是這就會引入額外的中間組件。

假如使用Redis,則通過Redis的INCR/INCRBY自增原子操作命令,能保證生成的ID肯定是唯一有序的,本質上實現方式與數據庫一致。

但是,超高併發場景,分佈式自增主鍵的生產性能,沒有本地生產ID的性能高。

總結一下,分佈式、高性能的中間件生成ID的優缺點和使用場景:

  • 優點:

    整體吞吐量比數據庫要高。

  • 缺點:

    Redis實例或集羣宕機後,找回最新的ID值有點困難。

  • 適用場景:

    比較適合計數場景,如用戶訪問量,訂單流水號(日期+流水號)等。

方案4:UUID、GUID生成ID

UUID:

按照OSF制定的標準計算,用到了以太網卡地址、納秒級時間、芯片ID碼和許多可能的數字。由以下幾部分的組合:當前日期和時間(UUID的第一個部分與時間有關,如果你在生成一個UUID之後,過幾秒又生成一個UUID,則第一個部分不同,其餘相同),時鐘序列,全局唯一的IEEE機器識別號(如果有網卡,從網卡獲得,沒有網卡以其他方式獲得)

GUID:

微軟對UUID這個標準的實現。UUID還有其它各種實現,不止GUID一種,不一一列舉了。

這兩種屬於不依賴數據源方式,真正的全球唯一性ID

總結一下,UUID、GUID生成ID的優缺點和使用場景:

  • 優點:

    不依賴任何數據源,自行計算,沒有網絡ID,速度超快,並且全球唯一。

  • 缺點:

    沒有順序性,並且比較長(128bit),作爲數據庫主鍵、索引會導致索引效率下降,空間佔用較多。

  • 適用場景:

    只要對存儲空間沒有苛刻要求的都能夠適用,比如各種鏈路追蹤、日誌存儲等。

方式5:snowflake算法(雪花算法)生成ID

snowflake ID 嚴格來說,屬於 本地生產 ID,這點和 Redis ID、MongoDB ID不同, 後者屬於遠程生產的ID。

本地生產ID性能高,遠程生產的ID性能低。

snowflake ID原理是使用Long類型(64位),按照一定的規則進行分段填充:時間(毫秒級)+集羣ID+機器ID+序列號,每段佔用的位數可以根據實際需要分配,其中集羣ID和機器ID這兩部分,在實際應用場景中要依賴外部參數配置或數據庫記錄。

總結一下,snowflake ID 的優缺點和使用場景:

  • 優點:

    高性能、低延遲、去中心化、按時間總體有序

  • 缺點:

    要求機器時鐘同步(到秒級即可),需要解決 時鐘回撥問題

    如果某臺機器的系統時鐘回撥,有可能造成 ID 衝突,或者 ID 亂序。

  • 適用場景:

    分佈式應用環境的數據主鍵

高併發ID的技術選型

這裏,不用地址的hash 編碼作爲ID

這裏,不用數據庫的自增長ID

這裏,不用redis、mongdb的分佈式ID

最終,

這裏,從發號性能、整體有序(B+樹索引結構更加友好)的角度出發,最終選擇的snowflake算法

snowflake算法的吞吐量在 100W ops +

但是 snowflake算法 問題是啥呢?需要解決時鐘回撥的問題。

如何解決時鐘回撥的問題,可以參考 推特官方的 代碼、 百度ID的代碼、Shardingjdbc ID的源碼,綜合存儲方案設計解決。

5、數據存儲的高併發架構

這個數據,非常的結構化,可以使用結構化數據庫MYSQL存儲。

結構非常簡單,我們會有二列:

1. ID,int,   // 分佈式雪花id;

2. SURL,varchar,  // 原始URL;

接下來,開始高併發、海量數據場景,需要進行 MYSQL存儲 的分庫分表架構。

陳某提示,這裏可以說說自己的分庫分表 操作經驗,操作案例。

然後進行 互動式作答。

也就是,首先是進行 輸入條件 詢問,並且進行確認。

然後按照分治模式,進行兩大維度的分析架構:

  • 數據容量(存儲規模) 的 分治架構、
  • 訪問流量 (吞吐量規模)的 分治架構。

這塊內容涉的方案,不同的項目,基本是相通的。

6、二義性檢查的高併發架構

所謂的地址二義性,就行同一個長址多次請求得到的短址不一樣。

在生產地址的時候,需要進行二義性檢查,防止每次都會重新爲該長址生成一個短址,一個個長址多次請求得到的短址是不一樣。

通過二義性檢查,實現長短鏈接真正意義上的一對一。

怎麼進行 二義性檢查?

最簡單,最爲粗暴的方案是:直接去數據庫中檢查

但是,這就需要付出很大的性能代價。

要知道:

數據庫主鍵不是 原始url,而是 短鏈url 。

如果根據 原始url 去進行存在性檢查,還需要額外建立索引。

問題的關鍵是,數據庫性能特低,沒有辦法支撐超高併發 二義性檢查

所以,這裏肯定不能每次用數據庫去檢查。

這裏很多同學可能會想到另一種方案,就是 redis 的布隆過濾, 把已經生成過了的 原始url,

大致的方案是,可以把已經生成過的 原始url ,在 redis 布隆過濾器中進行記錄。

每次進行二義性檢查,走redis 布隆過濾器。

布隆過濾器就是bitset+多次hash的架構,宏觀上是空間換時間,不對所有的 surl (原始url)進行內容存儲,只對surl進行存在性存儲,這樣就節省大家大量的內存空間。

在數據量比較大的情況下,既滿足時間要求,又滿足空間的要求。

布隆過濾器的巨大用處就是,能夠迅速判斷一個元素是否在一個集合中。

布隆過濾器的常用使用場景如下:

  1. 黑名單 : 反垃圾郵件,從數十億個垃圾郵件列表中判斷某郵箱是否垃圾郵箱(同理,垃圾短信)
  2. URL去重 : 網頁爬蟲對 URL 的去重,避免爬取相同的 URL 地址
  3. 單詞拼寫檢查
  4. Key-Value 緩存系統的 Key 校驗 (緩存穿透) : 緩存穿透,將所有可能存在的數據緩存放到布隆過濾器中,當黑客訪問不存在的緩存時迅速返回避免緩存及 DB 掛掉。
  5. ID 校驗,比如訂單系統查詢某個訂單 ID 是否存在,如果不存在就直接返回。

Bloom Filter 專門用來解決我們上面所說的去重問題的,使用 Bloom Filter 不會像使用緩存那麼浪費空間。

當然,他也存在一個小小問題,就是不太精確。

規則是:存在不一定存在,說不存在一定不存在

Bloom Filter 相當於是一個不太精確的 set 集合,我們可以利用它裏邊的 contains 方法去判斷某一個對象是否存在,但是需要注意,這個判斷不是特別精確。

一般來說,通過 contains 判斷某個值不存在,那就一定不存在,但是判斷某個值存在的話,則他可能不存在。

那麼對於 surl,處理的方案是:

  • 如果 redis bloom filter 不存在,直接生成
  • 否則,如果 redis bloom filter 判斷爲存在,可能是誤判,還需要進行db的檢查。

但是, redis bloom filter誤判的概率很低,合理優化之後,也就在1%以下。

可能有小夥伴說,如果100Wqps,1%也是10W1ps,DB還是扛不住,怎麼辦?

可以使用緩存架構,甚至多級緩存架構

具體來說,可以使用 Redis 緩存進行 熱門url的緩存,實現部分地址的一對一緩存

比如將最近/最熱門的對應關係存儲在K-V數據庫中,比如在本地緩存 Caffeine中存儲最近生成的長對短的對應關係,並採用過期機制實現 LRU 淘汰,從而保證頻繁使用的 URL 的總是對應同一個短址的,但是不保證不頻繁使用的URL的對應關係,從而大大減少了空間上的消耗。

7、映射模塊(/轉換模塊)高併發架構

這裏,主要是介紹自己對 多級緩存的 掌握和了解。

可以使用了緩存,二級緩存、三級緩存,加快id 到 surl的轉換。

簡單的緩存方案

將熱門的長鏈接(需要對長鏈接進來的次數進行計數)、最近的長鏈接(可以使用 Redis 保存最近一個小時的數據)等等進行一個緩存,如果請求的長URL命中了緩存,那麼直接獲取對應的短URL進行返回,不需要再進行生成操作

補充服務間的重定向301 和 302 的不同

301永久重定向和 302 臨時重定向。

  • 301永久重定向:第一次請求拿到長鏈接後,下次瀏覽器再去請求短鏈的話,不會向短網址服務器請求了,而是直接從瀏覽器的緩存裏拿,減少對服務器的壓力。
  • 302臨時重定向:每次去請求短鏈都會去請求短網址服務器(除非響應中用 Cache-Control 或 Expired 暗示瀏覽器進行緩存)

使用 301 雖然可以減少服務器的壓力,但是無法在 server 層獲取到短網址的訪問次數了,如果鏈接剛好是某個活動的鏈接,就無法分析此活動的效果以及用於大數據分析了。

而 302 雖然會增加服務器壓力,但便於在 server 層統計訪問數,所以如果對這些數據有需求,可以採用 302,因爲這點代價是值得的,但是具體採用哪種跳轉方式,還是要結合實際情況進行選型。

8、架構的魅力

架構魅力,在於沒有最好的方案,只有更好的方案,大家如果有疑問,或者更好的方案,可以多多交流。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章