Appboy là nền tảng tự động hóa tiếp thị hàng đầu thế giới dành cho các ứng dụng dành cho thiết bị di động. Chúng tôi thu thập hàng tỷ điểm dữ liệu mỗi tháng bằng cách theo dõi những gì người dùng đang làm trong các ứng dụng dành cho thiết bị di động của khách hàng và cho phép họ nhắm mục tiêu người dùng về email, thông báo đẩy và tin nhắn trong ứng dụng dựa trên hành vi hoặc nhân khẩu học của họ. MongoDB cung cấp năng lượng cho hầu hết ngăn xếp cơ sở dữ liệu của chúng tôi và chúng tôi lưu trữ hàng chục phân đoạn trên nhiều cụm tại ObjectRocket.
Một chiến lược tối ưu hóa hiệu suất phổ biến với MongoDB là sử dụng tên trường ngắn trong tài liệu. Đó là, thay vì tạo một tài liệu giống như thế này…
{first_name: "Jon", last_name: "Hyman"}
… Sử dụng các tên trường ngắn hơn để tài liệu có thể trông giống như…
{fn: "Jon", ln: "Hyman"}
Vì MongoDB không có khái niệm về cột hoặc lược đồ xác định trước, nên cấu trúc này rất thuận lợi vì tên trường được trùng lặp trên mọi tài liệu trong cơ sở dữ liệu. Nếu bạn có một triệu tài liệu mà mỗi tài liệu có trường “first_name”, bạn đang lưu trữ chuỗi đó một triệu lần. Điều này dẫn đến nhiều không gian hơn cho mỗi tài liệu, điều này cuối cùng ảnh hưởng đến số lượng tài liệu có thể phù hợp với bộ nhớ và ở quy mô lớn, có thể ảnh hưởng một chút đến hiệu suất, vì MongoDB phải ánh xạ tài liệu vào bộ nhớ khi nó đọc chúng.
Ngoài việc thu thập dữ liệu sự kiện, Appboy còn cho phép khách hàng của chúng tôi lưu trữ cái mà chúng tôi gọi là “thuộc tính tùy chỉnh” trên mỗi người dùng của họ. Ví dụ:một ứng dụng thể thao có thể muốn lưu trữ "Cầu thủ yêu thích" của người dùng, trong khi ứng dụng tạp chí hoặc báo có thể lưu trữ cho dù khách hàng có phải là "Người đăng ký hàng năm". Tại Appboy, chúng tôi có một tài liệu cho từng người dùng cuối của một ứng dụng mà chúng tôi theo dõi và trên đó, chúng tôi lưu trữ các thuộc tính tùy chỉnh đó cùng với các trường như họ hoặc tên của họ. Để tiết kiệm dung lượng và cải thiện hiệu suất, chúng tôi rút ngắn tên trường của mọi thứ chúng tôi lưu trữ trên tài liệu. Đối với các trường chúng tôi biết trước (chẳng hạn như tên, email, giới tính, v.v.), chúng tôi có thể tạo bí danh của riêng mình (ví dụ:“fn” có nghĩa là “tên”), nhưng chúng tôi không thể dự đoán tên của các thuộc tính tùy chỉnh mà khách hàng của chúng tôi sẽ ghi lại. Nếu khách hàng quyết định tạo thuộc tính tùy chỉnh có tên “supercalifragilisticexpialidocious”, chúng tôi không muốn lưu trữ thuộc tính đó trên tất cả các tài liệu của họ.
Để giải quyết vấn đề này, chúng tôi mã hóa các tên trường thuộc tính tùy chỉnh bằng cách sử dụng cái mà chúng tôi gọi là “cửa hàng tên”. Về mặt hiệu quả, đó là một tài liệu trong MongoDB ánh xạ các giá trị như “Người chơi yêu thích” thành một chuỗi rất ngắn, duy nhất, có thể dự đoán được. Chúng tôi có thể tạo bản đồ này chỉ bằng các toán tử nguyên tử của MongoDB
Lược đồ tài liệu lưu trữ tên cực kỳ cơ bản:có một tài liệu cho mỗi khách hàng và mỗi tài liệu chỉ có một trường mảng có tên là “danh sách”. Ý tưởng là mảng sẽ chứa tất cả các giá trị cho các thuộc tính tùy chỉnh và chỉ mục của một chuỗi nhất định sẽ là mã thông báo của nó. Vì vậy, nếu chúng ta muốn dịch "Người chơi yêu thích" thành một tên trường ngắn gọn, có thể dự đoán được, chúng ta chỉ cần kiểm tra "danh sách" để xem vị trí của nó trong mảng. Nếu không có ở đó, chúng ta có thể đưa ra một lệnh đẩy nguyên tử để thêm phần tử vào cuối mảng, (db.custom_attribute_name_stores.update ({_ id:X, list:{$ ne:“Người chơi yêu thích”}}, {$ push:{list:“Favourite Player”})), tải lại tài liệu và xác định chỉ mục. Tốt nhất, chúng tôi sẽ sử dụng $ addToSet, nhưng $ addToSet không đảm bảo thứ tự, trong khi $ push được ghi lại để thêm vào cuối theo mặc định.
Vì vậy, tại thời điểm này, chúng tôi có thể dịch một cái gì đó như “Người chơi yêu thích” thành một giá trị số nguyên. Giả sử giá trị đó là 1. Sau đó, tài liệu người dùng của chúng tôi sẽ giống như sau:
{
fn: "Jon",
ln: "Hyman",
custom: {
1: "LeBron James"
}
}
Tên trường ngắn gọn và gọn gàng! Một tác dụng phụ tuyệt vời của việc này là chúng tôi không phải lo lắng về việc khách hàng của mình sử dụng các ký tự mà MongoDB không thể hỗ trợ nếu không thoát, chẳng hạn như ký hiệu đô la hoặc dấu chấm.
Bây giờ, bạn có thể nghĩ rằng MongoDB cảnh báo với các tài liệu liên tục phát triển và tài liệu lưu trữ tên của chúng tôi có thể phát triển không giới hạn. Trên thực tế, chúng tôi đã mở rộng triển khai một chút để có thể lưu trữ nhiều hơn một tài liệu cho mỗi khách hàng. Điều này cho phép chúng tôi đặt giới hạn hợp lý về số lượng phần tử mảng mà chúng tôi cho phép trước khi tạo một tài liệu mới. Phần tốt nhất là chúng ta vẫn có thể làm tất cả những điều này một cách nguyên tử chỉ bằng cách sử dụng MongoDB! Để đạt được điều này, chúng tôi thêm một trường khác vào mỗi tài liệu được gọi là “giá trị nhỏ nhất”. Trường "giá trị nhỏ nhất" biểu thị số lượng phần tử đã được thêm vào tài liệu trước đó trước khi tài liệu này được tạo. Vì vậy, nếu chúng tôi thấy một tài liệu có “giá trị nhỏ nhất” 100 và “danh sách” [“Người giữ vé mùa”, “Người chơi yêu thích”], thì giá trị mã thông báo cho “Người chơi yêu thích nhất” là 101 (chúng tôi đang sử dụng 0 -based indexing). Trong ví dụ này, chúng tôi chỉ lưu trữ 100 giá trị trong mảng "danh sách" trước khi tạo một tài liệu mới. Bây giờ, khi chèn, chúng tôi sửa đổi một chút lực đẩy để hoạt động trên tài liệu với mức “thấp nhất cao nhất giá trị value ”và cũng đảm bảo rằng“ list.99 ”không tồn tại (nghĩa là không có gì trong chỉ mục 99 trong mảng“ list ”). Nếu một phần tử đã tồn tại ở chỉ mục đó, thì thao tác đẩy sẽ không làm gì cả. Trong trường hợp đó, chúng tôi biết rằng chúng tôi cần tạo một tài liệu lưu trữ tên mới với “giá_trị_cấp nhất” bằng tổng số phần tử tồn tại trên tất cả các tài liệu. Sử dụng một nguyên tử $ findAndModify, chúng tôi có thể tạo tài liệu mới nếu nó không tồn tại, tìm nạp nó trở lại và sau đó thử lại $ push một lần nữa.
Nếu khách hàng của chúng tôi có nhiều hơn một số thuộc tính tùy chỉnh, thì việc đọc lại tất cả các tài liệu lưu trữ tên để dịch từ các giá trị sang mã thông báo có thể tốn kém về băng thông và quá trình xử lý. Tuy nhiên, vì giá trị mã thông báo của một trường nhất định luôn giống nhau khi nó đã được tính toán, chúng tôi lưu mã thông báo vào bộ nhớ cache để tăng tốc độ dịch.
Chúng tôi đã áp dụng mô hình “mã thông báo cửa hàng tên” trong các phần khác nhau của ứng dụng của mình để cắt giảm kích thước tên trường trong khi tiếp tục sử dụng giản đồ linh hoạt. Nó cũng có thể hữu ích cho các giá trị. Giả sử rằng một ứng dụng đài phát thanh lưu trữ một thuộc tính tùy chỉnh là một mảng gồm 50 nghệ sĩ biểu diễn hàng đầu mà người dùng lắng nghe. Thay vì có một mảng với 50 chuỗi trong đó, chúng ta có thể mã hóa tên đài phát thanh và lưu trữ một mảng gồm 50 số nguyên trên người dùng. Việc truy vấn người dùng thích một nghệ sĩ nhất định hiện bao gồm hai lần tra cứu mã thông báo:một cho tên trường và một cho giá trị. Nhưng vì chúng tôi lưu bản dịch từ giá trị sang mã thông báo trong bộ nhớ cache, chúng tôi có thể sử dụng nhiều lần truy cập trong lớp bộ nhớ cache của mình để duy trì một vòng lặp duy nhất tới bộ nhớ cache khi dịch bất kỳ số lượng giá trị nào.
Tối ưu hóa này chắc chắn thêm một số hướng và phức tạp, nhưng khi bạn lưu trữ hàng trăm triệu người dùng như chúng tôi làm tại Appboy, đó là một tối ưu hóa đáng giá. Chúng tôi đã tiết kiệm được hàng trăm gigabyte dung lượng SSD đắt tiền thông qua thủ thuật này.
Muốn tìm hiểu thêm? Tôi sẽ thảo luận về các nhà phát triển tại Appboy trong Hội nghị Rackspace Solve NYC vào ngày 18 tháng 9 tại Cipriani.