這是一個關於我們在多個 MySQL 伺服器上分割資料的技術研究。我們在 2012 年年初完成了這個分片方法,它仍然是我們今天用來儲存核心資料的系統。
在我們討論如何分割資料之前,讓我們先了解一下我們的資料。心情照明,巧克力草莓,星際迷航語錄…
Pinteres 是你感興趣的所有東西的發現引擎。從資料的角度來說,Pinterest 是世界上最大的人類興趣圖集。有超過 500 億的 Pin 被 Pin 友們保存在 10 億塊圖板上。用戶再次 Pin,喜歡其他人的 Pin(粗略地說是一個淺顯的複製品),關注其他 Pin 友,畫板和興趣,然後查看主頁上所訂閱 Pin 友的所有資訊。太好了!現在讓它擴大規模!
成長的痛苦
在 2011 年我們取得了成功。在 一些 評估報告裡,我們的發展比其他的新創公司快得多。在 2011 年 9 月,我們每項基礎設備都超出了負載。我們應用了一些 NoSQL 技術,所有這些技術都導致了災難性的後果。同時,大量用於讀取的 MySQL 從伺服器產生了大量令人惱火的 bugs,特別是快取。我們重構了整個資料儲存模式。為了使之有效,我們仔細制定了我們的要求。
業務要求
我們的整個系統需要非常穩定,易於操作且易於擴展。我們希望支援資料庫能從開始的小儲存量,能隨著業務發展而擴展。
所有 Pin 友 產生的內容在網站上必須隨時可以存取。
支援以決定的順序請求存取 N 個 Pin 在畫板中展示(像依照建立的時間,或依照使用者特定的順序)。對於喜歡的 Pin 友和 Pin 友的 Pin 清單等也能按照特定的順序展示。
為了簡單起見,更新一般要確保最好的效果。為了取得最終一致性,你需要一些額外的東西,例如分散式 交易日誌。這是一件有趣並(不)簡單的事。
解決想法及要點備註
解決方案由於需要將海量的資料切片分佈到多個資料庫實例上,不能使用關聯式資料庫的連線、外鍵或索引等方法整合整個資料。想想就知道,關聯的子查詢不能跨越不同的資料庫實例。
我們的方案需要負載平衡資料存取。我們憎恨資料遷移,尤其是逐一記錄進行遷移,因關係的複雜性,這樣非常容易發生錯誤且加重系統不必要的複雜性。如果必須要遷移數據,最好是邏輯節點集的整體遷移。
為了達到方案實施的可靠迅速,我們需要在我們的分散式資料平台上使用最易於實現、最健壯的技術方案。
每個實例上的所有的資料將會完全複製到一個從實例上,作為資料備份。我們使用的是高可用性的 MapReduce (分散式運算環境) 的 S3 。我們前端的業務邏輯存取後台數據,只存取資料庫的 主實例。永遠不要讓您的前端業務去讀寫訪問從實例 。因為它與 主實例 資料同步存在延遲,會造成莫名其妙的錯誤,一旦將資料切片並分佈,沒有一絲理由讓你前端業務從實例 上讀寫資料。
最後,我們需要精心設計一個優秀的方案產生和解析我們所有資料物件的 全域唯一識別( UUID ) 。
我們的切片方案
不管怎樣,我們需要設計符合我們需求的,健壯的,效能優良且可維護的資料分佈解決方案。換句話說,它不能稚嫩(未經廣泛驗證)。因此,我們的基礎設計建立在 MySQL 之上,請參閱 we chose a mature technology(選擇成熟技術) 。設計之初,我們自然會跳開不用那些號稱具有自動分佈(auto-scaling)新技術能力的資料庫產品,諸如MongoDB,Cassandra 和Membase 之類的產品,因為它們似乎實施簡單卻適用性太差(常常發生莫名其妙的錯誤導致崩潰)。
旁白:強烈建議從底層基礎入手,避免時髦新鮮的東東 — 紮實把 MySQL 學好用好。相信我,字字都是淚。
MySQL 是成熟、穩定且就是好使的關係型資料庫產品。不只我們用它,包括許多知名大公司也使用它作為後台資料支撐,儲存著海量的資料。 (譯註:大概幾年前,由於MySQL 隨著SUN 被Oracle 的收購,歸到Oracle 名下。許多公司,如google,facebook 等由於擔心MySQL 的開源問題,紛紛轉到由MySQL 原作者開發的另一個開源資料庫MariaDB 下)MySQL 支援我們對資料庫要求依序資料請求,查詢指定範圍資料及行(記錄)級上的事務處理的技術要求。 MySQL 有一堆功能特性,但我們不需要那些。由於 MySQL 本身就是個單體解決方案,但我們卻要把我們的資料切片。 (譯註:此處的意思是,一個單一實例管理海量的數據,勢必造成性能問題。現在把一個海量整體數據切片成一個單體數據集,需要一個強有力的技術解決方案,把一個個的單體整合成一個整體,提高效能還不出錯)以下是我們的設計方案:
我們起始使用8 台EC2 伺服器,每台伺服器都執行一個MySQL 實例:
每個MySQL 伺服器各自以主- 主備份( master-master replicated )到1 台冗餘主機作為災難復原。我們前台業務只從主服務實例讀 / 寫資料 。我建議你也這麼做,它簡化許多事情,避免延遲故障。 (譯註:主- 主備份( master-master replicated ) 是MySQL 資料庫本身提供的功能,指兩台機器互做備份的一種模式,相對其它模式,如主- 從備份,兩台機器資料完全一致,後台同步,每台機器有自己單獨IP 都可訪問,可並發讀/ 寫訪問。但原文作者一再強調的是雖然這兩台互為冗餘使用主- 主備份,都可訪問。但你邏輯上區分主- 從,永遠只從其中一個進行讀/ 寫。例如,圖中所示, MySQL001A 和MySQL001B 間主- 主備份,但你只從MySQL001A 進行讀/ 寫訪問。另:他們使用了16 台機器,另8 台做從機的可能不是EC2 也未必)
每個MySQL 實例可以有多個資料庫:
注意每個資料庫是如何唯一地命名為db00000,db00001,直到dbNNNN。每個資料庫都是我們資料庫的分片。我們做了一個設計,一旦一塊資料被分配到一個分片中,它就不會移出那個分片。但是,你可以透過將分片移動到其他機器來獲得更大的容量(我們將在後面討論這一點)。
我們維護一個設定資料庫表,此表中記錄這切片資料庫在哪台機器上:
[ {“range”: (0,511), “master”: “MySQL001A”, “slave”: “MySQL001B”}, {“range”: (512, 1023), “master”: “MySQL002A”, “slave”: “MySQL002B”}, ... {“range”: (3584, 4095), “master”: “MySQL008A”, “slave”: “MySQL008B”} ]
這個設定表僅當遷移切片資料庫或取代主機時修改。例如,一個主實例主機宕掉了,我們會提升它的從實例主機為主實例,然後儘快頂替一個新機器當從實例主機。設定腳本保留在 ZooKeeper 上,當出現上述修改時,透過腳本傳送到維護切片服務的機器上進行設定變更。 (譯註:可發現原作者一直強調的,前端業務僅從邏輯主實例讀寫資料的好處)。
每個切片資料庫保持相同的資料庫表及表格結構,諸如,有 pins ,boards ,users_has_pins ,users_likes_pins ,pin_liked_by_user 等資料庫表。在佈署時同步建構。
分佈資料到切片伺服器設計方案
我們組合切片ID(shard ID) 、資料類型識別和局部ID(local ID) 形成64 位元的全域唯一識別(ID) 。切片 ID(shard ID) 佔 16 個位元(bit), 資料類型識別佔 10 個位元(bit), 局部 ID(local ID) 佔 36 個位元 (bit)。明眼人馬上會發現,這才 62 位元。我過去的分佈及整合數據經驗告訴我,保留幾位留做擴充是無價寶。因此,我保留了 2 位元(設為 0)。 (譯註:這裡解釋一下,根據後面的運算和說明,任何對象的唯一標識ID 是64 位,最高2 位始終為0,之後是36 位的局部標識,之後是10 位類型標識,最後是16 位的切片標識。局部標識可表示2^36 達600 多億個ID 。資料類型可表示2^10 達1024 個物件類型,切片標識可細分成2^16 達65536 個切片資料庫。前面說的方案切了4096 個切片資料庫)
ID = (shard ID << 46) | (type ID << 36) | (local ID<<0) 以 Pin: https://www.pinterest.com/pin/241294492511... 为例,让我们解构这个 Pin 对象的 全局 ID 标识 241294492511762325 : Shard ID = (241294492511762325 >> 46) & 0xFFFF = 3429 Type ID = (241294492511762325 >> 36) & 0x3FF = 1 Local ID = (241294492511762325 >> 0) & 0xFFFFFFFFF = 7075733
可知這個Pin 物件在3429 切片資料庫裡。假設 Pin 物件 資料類型標識為 1,它的記錄在 3429 切片資料庫裡的 pin 資料表中的 7075733 記錄行中。舉例,假設切片 3429 資料庫在 MySQL012A 中,我們可利用下面語句得到其資料記錄:(譯註:這裡原作者泛泛舉例,若按其前面方案範例來說,3429 應在 MySQL007A 上)
conn = MySQLdb.connect(host=”MySQL012A”) conn.execute(“SELECT data FROM db03429.pins where local_id=7075733”)
有两种类型的数据:对象或关系。对象包含对象本身细节。 如 Pin 。
存储对象的数据库表
对象库表中的每个记录,表示我们前端业务中的一个对象,诸如:Pins(钉便签), users(用户),boards(白板)和 comments(注释),每个这样的记录在数据库表中设计一个标识 ID 字段(这个字段在表中作为记录的 自增主键「auto-incrementing primary key」 ,也就是我们前面提到的 局部 ID「 local ID」 ),和一个 blob 数据字段 -- 使用 JSON 保存对象的具体数据 --。
CREATE TABLE pins ( local_id INT PRIMARY KEY AUTO_INCREMENT, data TEXT, ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB;
举例,一个 Pin 对象形状如下:
{“details”: “New Star Wars character”, “link”: “http://webpage.com/asdf”, “user_id”: 241294629943640797, “board_id”: 241294561224164665, …}
创建一个 Pin 对象,收集所有的数据构成 JSON blob 数据。然后,确定它的 切片 ID「 shard ID」 (我们更乐意把 Pin 对象的切片数据放到跟其所在 白板「 board」 对象相同的切片数据库里,这不是强制设计规则)。Pin 对象的数据类型标识为 1。连接到 切片 ID 指示的切片数据库,插入(insert)Pin 对象的 JOSON 数据到 Pin 对象数据库表中,MySQL 操作成功后将会返回 自增主键「auto-incrementing primary key」 给你,这个作为此 Pin 对象的 局部 ID「 local ID」。现在,我们有了 shard 、类型值、local ID 这些必要信息,就可以构建出此 Pin 对象的 64 位 ID 。(译注:原作者提到的,他们的前端业务所用到的每种对象都保存在一个对象数据库表里,每个对象记录都通过一个全局唯一 ID 去找到它,但这个全局唯一 ID 并不是数据库表中的 局部 ID,由于切片的缘故。原作者一直在讲这个设计及其原理。这样设计的目的为了海量数据切片提高性能,还要易用,可维护,可扩展。后面,作者会依次讲解到)
编辑一个 Pin 对象,使用 MySQL 事务「transaction」 在 Pin 对象的数据记录上 读出 -- 修改 -- 写回「read-modify-write」 Pin 对象的 JOSON 数据字段:
> BEGIN > SELECT blob FROM db03429.pins WHERE local_id=7075733 FOR UPDATE [修改 json blob] > UPDATE db03429.pins SET blob=’<修改后的 blob>’ WHERE local_id=7075733 > COMMIT
编辑一个 Pin 对象,您当然可以直接删除这个对象在 MySQL 数据库表中的数据记录。但是,请仔细想一下,是否在对象的 JSON 数据上加个叫做「 active」的域,把剔除工作交由前端中间业务逻辑去处理或许会更好呢。
(译注:学过关系数据库的应知道,自增主键在记录表中是固实,在里面删除记录,会造成孔洞。当多了,势必造成数据库性能下降。数据库只负责保存数据和高性能地查询、读写数据,其数据间的关系完全靠设计精良的对象全局 ID 通过中间件逻辑去维护 这样的设计理念一直贯穿在作者的行文中。只有理解了这点您才能抓住这篇文章的核心)
关系映射数据库表
关系映射表表示的是前端业务对象间的关系。诸如:一个白板(board)上有哪些钉便签(Pin), 一个钉便签(Pin)在哪些白板(board)上等等。表示这种关系的 MySQL 数据库表包括 3 个字段:一个 64 位的「from」ID, 一个 64 位的「to」ID 和一个顺序号。每个字段上都做索引方便快速查询。其记录保存在根据「from」字段 ID 解构出来的切片 ID 指示出的切片数据库上。
CREATE TABLE board_has_pins ( board_id INT, pin_id INT, sequence INT, INDEX(board_id, pin_id, sequence) ) ENGINE=InnoDB;
(译注:这里的关系映射指前端业务对象间的关系用数据库表来运维,并不指我上节注释中说到的关系数据库的关系映射。作者开篇就讲到,由于切片,不能做关系数据库表间的关系映射的,如一对一,一对多,多对多等关系关联)
关系映射表是单向的,如 board_has_pins(板含便签)表方便根据 board (白板)ID 查询其上有多少 Pin(钉便签)。若您需要根据 Pin(钉便签)ID 查询其都在哪些 board(白板)上,您可另建个表 pin_owned_by_board(便签属于哪些白板)表,其中 sequence 字段表示 Pin 在 board 上的顺序号。(由于数据分布在切片数据库上,我们的 ID 本身无法表示其顺序)我们通常将一个新的 Pin 对象加到 board 上时,将其 sequence 设为当时的系统时间。sequence 可被设为任意整数,设为当时的系统时间,保证新建的对象的 sequence 总是大于旧对象的。这是个方便易行的方法。您可通过下面的语句从关系映射表中查询对象数据集:
SELECT pin_id FROM board_has_pins WHERE board_id=241294561224164665 ORDER BY sequence LIMIT 50 OFFSET 150
语句会查出 50 个 pin_ids(便签 ID ), 随后可用这些对象 ID 查询其具体信息。
我們只在業務應用層進行這些關係的映射,例如 board_id -> pin_ids -> pin objects (從 白板 ID -> 便簽 IDs -> 便條物件)。這種設計一個非常棒的功能是,您可以分開緩存這些關係映射對。例如,我們快取pin_id -> pin object (便簽ID -> 便條物件)關係映射在memcache(記憶體快取)叢集伺服器上,board_id -> pin_ids (白板ID -> 便條IDs)關係映射快取在redis集群伺服器上。這樣,可以非常適合我們優化快取技術策略。
增大服務能力
在我們的系統中,提升服務處理能力主要三個途徑。最容易的是升級機器(更大的空間,更快的硬碟速度,更多的內存,無論什麼解決系統瓶頸的升級都算)
另一個途徑,擴大切片範圍。最初,我們設計只切片了 4096 個資料庫,相較於我們設計的 16 位元的切片 ID,還有許多空間,因為 16 位元可表示 65536 個數。某些時間節點,若我們再提供 8 台機器運行 8 個 MySQL 資料庫實例,提供從 4096 到 8192 的切片資料庫,之後,新的資料將只往這個區間的切片資料庫上存放。平行計算的資料庫有 16 台,服務能力必然提升。
最後的途徑,遷移切片資料庫主機到新切片主機(局部切片擴充)以提升能力。例如,我們想將前例中的 MySQL001A 切片主機(其上是 0 到 511 編號的切片資料庫)擴充分佈到 2 台切片主機上。同我們設計地,我們建立一個新的 master-master 互備份主機對作為新切片主機(命名為 MySQL009A 和 B)並從 MySQL001A 上整體複製資料。
當資料複製完成後,我們修改切片配置,MySQL001A 只負責 0 到 255 的切片資料庫,MySQL009A 只負責 256 到 511 的切片資料庫。現在 2 台中每台主機只負責過去主機負責的一半的任務,服務能力提升。
一些特性說明
對於舊系統已產生的業務物件數據,要根據設計,對業務物件要產生它們在新系統中的UUIDs,你應該意識到它們放到哪裡(哪個切片資料庫)由你決定。 (譯註:你可以規劃舊資料在切片資料庫上的分佈)但是,在放入到切片資料庫時,只有在插入記錄時,資料庫才會傳回插入物件的local ID,有了這個,才能建構物件的UUID 。
(譯註:在遷移時要考慮好業務物件間關係的建立,透過UUID)
對於那些在已有大量資料的資料庫表,曾使用過修改表結構類別指令(ALTERs)-- 諸如添加個字段之類的-- 的人來說,您知道那是一個非常漫長和痛苦的過程。我們的設計是絕不使用 MySQL 上 ALTERs 等級的指令(當已有資料時)。在我們的業務系統 Pinterest 上,我們使用最後一個 ALTER 語句大概是在 3 年前了。對於物件表中對象,如果您需要新增個物件屬性字段,您將新增至物件資料的 JOSON blob 欄位。您可以為新物件屬性設定個預設值,當存取舊物件的資料時,若舊物件沒有新屬性,您可以為其新增上新屬性預設值。對於關係映射表來說,乾脆,直接建立新的關係映射表以符合您的需求。這些您都清楚了!讓您的系統揚帆起行吧!
模轉(mod)資料庫的切片
模轉資料切片(mod shard)名稱只是像 Mod Squad,實則完全不同。
有些業務物件需要透過非 ID (non-ID)的方式查詢存取。 (譯註: 此 ID 指先前設計說明中的 64 位 UUID)舉例來說,如果一位 Pin 友(Pinner)是以他(她)的 facebook 註冊帳號註冊登入我們的業務平台上的。我們需要將其 facebook ID 與我們的 Pin 友(Pinner)的 ID 映射。 facebook ID 對於我們系統只是一串二進位的數。 (譯註:暗示我們不能像我們系統平台的設計那樣解構別的平台的ID,也談不上如何設計切片,只是把它們保存起來,並設計使之與我們的ID 映射)因此,我們需要保存它們,也需要把它們分別保存在切片資料庫上。我們稱之為模轉資料切片(mod shard)其它的例子還包括 IP 位址、使用者名稱和使用者電子郵件等。
模转数据切片(mod shard)类似前述我们业务系统的数据切片设计。但是,你需要按照其输入的原样进行查询。如何确定其切片位置,需要用到哈希和模数运算。哈希函数将任意字串转换成定长数值,而模数设为系统已有切片数据库的切片数量,取模后,其必然落在某个切片数据库上。结果是其数据将保存在已有切片数据库上。举例:
shard = md5(“1.2.3.4") % 4096
(译注:mod shard 这个词,我网上找遍了,试图找到一个较准确权威的中文翻译!无果,因为 mod 这个词有几种意思,最近的是 module 模块、模组,同时它也是模运算符(%)。我根据原文意思,翻译为 模转 。或可翻译为 模式,但个人感觉意思模糊。不当之处,请指正。另,原作者举的例子是以 IP 地址举例的,哈希使用的是 md5,相比其它,虽老但性能最好)
在这个例子中分片是 1524。 我们维护一个类似于 ID 分片的配置文件:
[{“range”: (0, 511), “master”: “msdb001a”, “slave”: “msdb001b”}, {“range”: (512, 1023), “master”: “msdb002a”, “slave”: “msdb002b”}, {“range”: (1024, 1535), “master”: “msdb003a”, “slave”: “msdb003b”}, …]
因此,为了找到 IP 为 1.2.3.4 的数据,我们将这样做:
conn = MySQLdb.connect(host=”msdb003a”) conn.execute(“SELECT data FROM msdb001a.ip_data WHERE ip='1.2.3.4'”)
你失去了一些分片好的属性,例如空间位置。你必须从一开始就设置分片的密钥(它不会为你制作密钥)。最好使用不变的 id 来表示系统中的对象。这样,当用户更改其用户名时,您就不必更新许多引用。
最后的提醒
这个系统作为 Pinterest 的数据支撑已良好运行了 3.5 年,现在看来还会继续运行下去。设计实现这样的系统是直观、容易的。但是让它运行起来,尤其迁移旧数据却太不易了。若您的业务平台面临着急速增长的痛苦且您想切片自己的数据库。建议您考虑建立一个后端集群服务器(优先建议 pyres)脚本化您迁移旧数据到切片数据库的逻辑,自动化处理。我保证无论您想得多周到,多努力,您一定会丢数据或丢失数据之间的关联。我真的恨死隐藏在复杂数据关系中的那些捣蛋鬼。因此,您需要不断地迁移,修正,再迁移... 你需要极大的耐心和努力。直到您确定您不再需要为了旧数据迁移而往您的切片数据库中再操作数据为止。
这个系统的设计为了数据的分布切片,已尽最大的努力做到最好。它本身不能提供给你数据库事务 ACID 四要素中的 Atomicity(原子性)、Consistency(一致性)、Isolation(隔离性)哇呕!听起来很坏呀,不用担心。您可能不能利用数据库本身提供的功能很好地保证这些。但是,我提醒您,一切尽在您的掌握中,您只是让它运行起来,满足您的需要就好。设计简单直接就是王道,(译注:也许需要您做许多底层工作,但一切都在您的控制之中)主要是它运行起来超快! 如果您担心 A(原子性)、I(隔离性)和 C(一致性),写信给我,我有成堆的经验让您克服这些问题。
还有最后的问题,如何灾难恢复,啊哈? 我们创建另外的服务去维护着切片数据库,我们保存切片配置在 ZooKeeper 上。当单点主服务器宕掉时,我们有脚本自动地提升主服务器对应的从服务器立即顶上。之后,以最快的速度运行新机器顶上从服务器的缺。直至今日,我们从未使用过类似自动灾难恢复的服务。
推荐教程:《MySQL教程》
以上是MySQL 如何利用分片來解決 500 億資料的儲存問題的詳細內容。更多資訊請關注PHP中文網其他相關文章!