Cách Tôi Vận Hành Bot Discord Trên 45.000 Máy Chủ Miễn Phí

Bạn có bao giờ tự hỏi làm thế nào một bot Discord có thể hoạt động hiệu quả trên hàng chục nghìn máy chủ mà không tốn một xu nào không? Khi chia sẻ với các nhà phát triển khác về bot Discord của mình, tôi thường nhận được sự ngạc nhiên lớn khi tiết lộ rằng nó đang hoạt động miễn phí, sử dụng các gói miễn phí có sẵn rộng rãi trên các nền tảng đám mây, mặc dù nó đã hiện diện trong gần 45.000 máy chủ. Điều này dường như là một điều không tưởng đối với nhiều người.

Chính vì vậy, tôi quyết định viết bài viết này để chia sẻ chi tiết cách tôi làm được điều đó. Chúng ta sẽ cùng khám phá nơi bot được lưu trữ, những quyết định kỹ thuật ban đầu giúp bot trở nên nhẹ nhàng, các tối ưu hóa đã thực hiện và những cải tiến tiềm năng trong tương lai. Đây là một hành trình thú vị về việc khai thác tối đa tài nguyên miễn phí và tối ưu hóa hiệu suất.

Nền Tảng Lưu Trữ Bot: Khám Phá Sức Mạnh Của Dịch Vụ Đám Mây Miễn Phí

Để vận hành một bot miễn phí, yếu tố quan trọng nhất, như bạn có thể đoán, chính là tìm kiếm dịch vụ lưu trữ miễn phí. May mắn thay, các nhà cung cấp đám mây lớn ngày nay thường cung cấp các gói miễn phí (free tier) vô cùng hào phóng, đủ sức đáp ứng nhu cầu của nhiều dự án cá nhân và quy mô nhỏ.

Bot của tôi hiện đang được lưu trữ trên Oracle Cloud. Gói miễn phí của họ cho phép bạn tạo một máy chủ riêng ảo (VPS) với cấu hình lên đến 24 GB RAM và 4 vCPU. Đây là một con số “điên rồ” khi so sánh với hầu hết các gói miễn phí khác từ các nhà cung cấp tương tự, thường chỉ cung cấp khoảng 1 GB RAM. Một điểm cần lưu ý là chỉ các lõi ARM mới có sẵn trong gói này.

Hiện tại, tôi đang sử dụng 18 GB RAM cho máy chủ bot và 3 vCPU. Không chỉ đủ, cấu hình này thực sự còn “quá mức cần thiết”. Tôi tin rằng tôi có thể mở rộng quy mô bot lên ít nhất 150.000 máy chủ mà không cần thay đổi bất kỳ điều gì về phần cứng hiện tại.

Oracle Cloud Free Tier Configuration

Mặc dù lõi ARM nghe có vẻ là một điểm trừ, nhưng các bài kiểm tra hiệu năng tôi đã chạy trước khi chuyển bot sang đây thực sự cho thấy các lõi ARM của Oracle vượt trội hơn so với lõi Intel và AMD tại Digital Ocean và Vultr (ngay cả các gói “tần số cao” và “hiệu suất cao” của họ), ít nhất là trong các bài kiểm tra SQLite và render hình ảnh mà tôi đã sử dụng. Các lõi ARM cũng tỏ ra rất tiết kiệm chi phí ngay cả khi bạn phải trả tiền. Thành thật mà nói, tôi khá ấn tượng với dịch vụ của Oracle. Thời gian ngừng hoạt động cũng rất ít; tôi chỉ nhớ một lần ngừng hoạt động kéo dài khoảng 4 giờ trong suốt 2 năm bot hoạt động tại đây.

Tuy nhiên, có một lưu ý nhỏ: nếu bạn tìm kiếm trên Reddit và các diễn đàn khác, bạn có thể thấy một số câu chuyện về việc Oracle ngẫu nhiên tắt các tài khoản free tier. Tôi thực tế đã nâng cấp lên tài khoản trả phí và trả một khoản nhỏ (khoảng 2 USD/tháng) cho bộ lưu trữ đối tượng để sao lưu tệp cá nhân bằng Duplicati. Điều này có thể giúp tôi nằm trong “danh sách tốt”. Cho đến nay, cá nhân tôi chưa gặp bất kỳ vấn đề nào.

Sử Dụng Google Cloud Run Functions Để Tối Ưu Hóa Bộ Nhớ

Mặc dù phiên bản Oracle miễn phí hiện tại đã quá dư thừa, nhưng trước đây, tôi đã sử dụng các hàm Cloud Run để tối ưu hóa bộ nhớ cho bot của mình. Một vài tính năng của bot, đặc biệt là những tính năng yêu cầu nhiều bộ nhớ, được triển khai dưới dạng Google Cloud Run functions (đây là giải pháp của Google cho AWS Lambda).

Cả hai tính năng được triển khai theo cách này đều sử dụng vài trăm megabyte RAM để lưu trữ các từ điển trong bộ nhớ. Bằng cách triển khai các tính năng đó dưới dạng hàm Cloud Run, máy chủ bot chính không cần phải cấp phát RAM cho chúng. Tất cả đều được khởi tạo trên một máy chủ riêng biệt ở đâu đó trong đám mây kỳ diệu của Google, và máy chủ bot chính chỉ cần thực hiện một yêu cầu HTTP đến hàm Cloud Run đó.

Về cơ bản, tôi sử dụng Cloud Run functions như một phần RAM miễn phí bổ sung cho các tính năng tiêu tốn nhiều bộ nhớ này (và cả chu kỳ CPU miễn phí, mặc dù điều đó ít quan trọng hơn vì tôi luôn có nhiều dung lượng CPU). Các tính năng này đôi khi có thể cảm thấy chậm hơn một chút do hiện tượng khởi động nguội (cold start), nhưng đó không phải là một vấn đề lớn.

Gói miễn phí của Google Cloud rất hào phóng, dễ dàng đáp ứng tất cả các nhu cầu sử dụng Cloud Run functions của tôi.

Ghi Nhật Ký Và Báo Cáo Lỗi: Hệ Thống Giám Sát Chủ Động

Việc giám sát và phát hiện lỗi kịp thời là cực kỳ quan trọng để duy trì hoạt động ổn định của bot. Đối với bot của mình, tôi đã triển khai một hệ thống ghi nhật ký và báo cáo lỗi hiệu quả, tất cả đều nằm trong gói miễn phí của các dịch vụ đám mây.

Các nhật ký (logs) được gửi đến Google Cloud Logging. Từ đó, các nhật ký lỗi (error level logs) sẽ tự động chuyển đến Google Cloud Error Reporting. Một lần nữa, tất cả những dịch vụ này đều được bao gồm trong gói miễn phí của Google Cloud, với rất nhiều dung lượng dự phòng.

Ngoài ra, tôi còn có một bot riêng biệt trong máy chủ hỗ trợ của mình cho phép các quản trị viên kích hoạt PagerDuty. Hệ thống này sẽ gọi điện thoại di động của tôi và đánh thức tôi nếu có bất kỳ tình huống đặc biệt nghiêm trọng nào xảy ra. May mắn thay, tính năng này chưa bao giờ cần phải sử dụng. Như bạn có thể đoán, dịch vụ này cũng nằm trong gói miễn phí của PagerDuty.

Ngôn Ngữ Và Môi Trường Thực Thi: JavaScript và Node.js

Việc lựa chọn ngôn ngữ lập trình và môi trường thực thi đóng vai trò quan trọng trong hiệu suất và khả năng mở rộng của bot. Bot của tôi được xây dựng bằng JavaScript và chạy với Node.js.

Mặc dù JavaScript nói chung không phải là một ngôn ngữ đặc biệt nhanh, nhưng nhiều công việc nặng nhọc của bot lại phát huy rất tốt sức mạnh của Node.js. Phần lớn công việc này xảy ra trong mã gốc được tối ưu hóa cao bên trong công cụ V8 engine, trái tim của Node.js.

Ví dụ, việc phân tích cú pháp JSON (mà bot cần thực hiện cho mỗi sự kiện nhận được từ Discord) được triển khai bằng C++ trong V8 engine và cực kỳ nhanh. Chi phí sau đó để truyền đối tượng đã được phân tích cú pháp sang môi trường JavaScript là tương đối nhỏ, và công việc được thực hiện bởi mã JavaScript cũng khá tối thiểu trong hầu hết các trường hợp (chúng ta sẽ tìm hiểu kỹ hơn về điều này sau).

Node.js có thể không phải là lựa chọn tối ưu nhất ở đây, và một bot được viết bằng C++ hoặc Rust có thể hoạt động tốt hơn (tùy thuộc vào thư viện bot). Tuy nhiên, Node.js có khả năng vượt trội so với các lựa chọn phổ biến khác bao gồm Python, Java và C#, và thậm chí có thể cạnh tranh sòng phẳng với Go. (Xin lỗi, tôi không có các bài kiểm tra hiệu năng cụ thể, đây chỉ là đánh giá dựa trên kinh nghiệm).

Lựa Chọn Thư Viện Bot: Quyết Định Nền Tảng Quan Trọng

Việc chọn một thư viện bot quan trọng không kém, và thậm chí có thể quan trọng hơn, so với việc chọn ngôn ngữ lập trình.

Bối cảnh: API của Discord bao gồm API websocket (để nhận các sự kiện theo thời gian thực) và API REST (để thực hiện các hành động). Cả hai đều không dễ sử dụng, đặc biệt là API websocket. May mắn thay, có các thư viện được cộng đồng duy trì cho hầu hết các ngôn ngữ lập trình phổ biến để đơn giản hóa việc tương tác với các API của Discord.

Các nhà phát triển bot hầu như luôn chọn một trong các thư viện này thay vì cố gắng tự phát triển lại từ đầu. Khi tôi đưa ra lựa chọn này vào năm 2016, có hai lựa chọn chính cho Node.js: discord.jsEris.

* discord.js nổi tiếng với việc có nhiều tính năng hơn, tài liệu tuyệt vời và một cộng đồng thân thiện.
* Eris được biết đến là nhanh hơn và nhẹ hơn nhiều, có một cộng đồng hơi “khép kín” hơn, và được sử dụng bởi hầu hết các bot Node.js phổ biến cần khả năng mở rộng theo chiều dọc.

Vào thời điểm đó, tôi không có tham vọng mở rộng bot của mình lên hàng chục nghìn máy chủ, nhưng nhiều bot mà tôi biết và tôn trọng đều sử dụng Eris, và tôi không e ngại danh tiếng của nó vì tôi không phải là người mới bắt đầu. Vì vậy, tôi đã chọn Eris, và đó chắc chắn là lựa chọn đúng đắn khi nhìn lại.

Ngày nay, discord.js đã cải thiện hiệu suất của mình ở một mức độ nào đó, trong khi Eris đã bị phân mảnh do các nhà bảo trì chính dường như đã chuyển đi. Bot của tôi hiện đang sử dụng Dysnomia, đây là một nhánh của Eris nhằm tiếp tục phát triển, nhưng đây là một thiết lập khá đặc thù vào thời điểm này.

Tối Ưu Hóa Thư Viện Để Tiết Kiệm Bộ Nhớ Và Băng Thông

Cách một thư viện bot quản lý dữ liệu cache là một yếu tố chính quyết định mức độ mở rộng của bot. Eris (và sau đó là Dysnomia) tương đối hiệu quả trong việc này so với các thư viện bot khác. Mặt khác, các tùy chọn tùy chỉnh cache của nó không toàn diện lắm. Tuy nhiên, có một tùy chọn nó cung cấp là cho phép bạn tùy chỉnh kích thước cache tin nhắn, và đây là một tùy chọn khá quan trọng.

* Kích thước Cache Tin Nhắn: Tôi đặt kích thước cache tin nhắn về 0 trong bot của mình, vì bot của tôi không bao giờ cần truy cập các tin nhắn đã gửi trước đó. Nếu tôi nhớ không lầm, mặc định là lưu 100 tin nhắn gần đây nhất trong mỗi kênh. Đối với một bot trong 45.000 máy chủ, con số đó dễ dàng lên đến hàng triệu tin nhắn và có thể chiếm nhiều gigabyte bộ nhớ. Bằng cách tắt cache này, tôi tiết kiệm được một lượng RAM khổng lồ.


    // Ví dụ về cách cấu hình Eris/Dysnomia để tắt cache tin nhắn
    const bot = new Eris("YOUR_BOT_TOKEN", {
        messageLimit: 0, // Tắt cache tin nhắn
        intents: [
            // Chỉ bật các Gateway Intents cần thiết
            "guilds",
            "guildMessages",
            "directMessages"
            // Tránh "guildPresences" nếu không thực sự cần thiết
        ]
    });
    

* Chỉ Bật Gateway Intents Cần Thiết: Một tối ưu hóa quan trọng khác là chỉ bật các Gateway Intents mà bạn thực sự cần. Nếu có thể, hãy tránh Intent PRESENCE_UPDATE như tránh bệnh dịch. Việc bật Intent này có thể làm tăng đáng kể số lượng sự kiện mà bot của bạn nhận được (bạn sẽ nhận được một sự kiện bất cứ khi nào trạng thái trực tuyến của bất kỳ ai thay đổi trong bất kỳ máy chủ nào). Hiện tại, việc bật PRESENCE_UPDATE thực sự yêu cầu sự chấp thuận từ Discord nếu bot của bạn có mặt trong hơn 100 máy chủ, vì vậy đây là một quyết định mà hầu hết các nhà phát triển không cần phải bận tâm – Discord sẽ buộc tắt nó cho bạn trừ khi bạn thực sự cần.

Tối Ưu Hóa “Đường Dẫn Nóng” (The Hot Path)

Bot nhận một sự kiện cho mỗi tin nhắn được gửi trong bất kỳ máy chủ nào mà nó có mặt. Điều đó trung bình khoảng 100-200 sự kiện mỗi giây. Ít nhất 99,9% các tin nhắn đó không dành cho bot, vì vậy bot chỉ cần nhìn vào tin nhắn, quyết định “cái này không dành cho mình”, và không làm gì cả. Chúng ta càng làm điều đó nhanh chóng thì càng tốt. Dưới đây là một phần mã chính liên quan đến việc đó:


processInput(bot, msg) {
  let serverId = msg.channel.guild ? msg.channel.guild.id : msg.channel.id;
  let prefixes = this.persistence_.getPrefixesForServer(serverId); // Điểm tối ưu quan trọng
  let msgContent = msg.content;

  msgContent = msgContent.replace('\u3000', ' ');
  let spaceIndex = msgContent.indexOf(' ');
  let commandText = '';
  if (spaceIndex === -1) {
    commandText = msgContent;
  } else {
    commandText = msgContent.substring(0, spaceIndex);
  }

  commandText = commandText.toLowerCase();

  for (let prefix of prefixes) {
    for (let command of this.commands_) {
      for (let alias of command.aliases) {
        const prefixedAlias = prefix + alias;
        if (commandText === prefixedAlias) {
          return this.executeCommand_(bot, msg, command, msgContent, spaceIndex, prefix);
        }
      }
    }
  }

  return false;
}

Đoạn mã này thực sự không được tối ưu hóa quá mức ở cấp độ vi mô. Nhìn vào nó bây giờ, tôi có thể tưởng tượng cách chúng ta có thể làm tốt hơn (chủ yếu bằng cách tránh tạo các chuỗi mới). Nhưng đây là phần quan trọng mà chúng ta đang làm đúng:


let prefixes = this.persistence_.getPrefixesForServer(serverId);

Dòng này đang lấy các tiền tố lệnh tùy chỉnh cho máy chủ Discord hiện tại. Nếu bạn chưa biết điều đó có nghĩa là gì, hãy xem xét rằng theo mặc định, tất cả các lệnh của bot đều được tiền tố/đặt tên bằng k!. Ví dụ: k!help, k!about, v.v. Nhưng điều gì sẽ xảy ra nếu có một bot khác sử dụng cùng tiền tố và nó cũng có lệnh k!help? May mắn thay, bot cho phép quản trị viên máy chủ thay đổi tiền tố k! thành một cái gì đó khác để tránh nhầm lẫn (hoặc nếu họ chỉ thích sử dụng một tiền tố khác).

Đây là cài đặt theo từng máy chủ được lưu trong cơ sở dữ liệu, nhưng đoạn mã này để lấy nó là đồng bộ, và đó là tối ưu hóa quan trọng. Tất cả các tiền tố lệnh đều được lưu vào bộ nhớ trong tiến trình để tránh cần bất kỳ giao tiếp giữa các tiến trình nào trong “đường dẫn nóng” này. Thực hiện hàng trăm cuộc gọi mỗi giây đến cơ sở dữ liệu hoặc phiên bản cache, mặc dù có thể, sẽ thêm tải đáng kể và sẽ ảnh hưởng cụ thể đến khả năng mở rộng.

Không phải tất cả các bot đều có thể tránh các truy vấn cơ sở dữ liệu trong “đường dẫn nóng” của chúng. Nếu bạn có một bot cấp XP cho người dùng mỗi khi họ gửi tin nhắn, thì bạn phải thực hiện truy vấn cơ sở dữ liệu, không có cách nào khác (mặc dù bạn có thể gom nhiều lần tăng XP lại với nhau và ghi chúng vào cơ sở dữ liệu theo nhóm).

Những Điểm Có Thể Cải Thiện: Đánh Đổi Giữa Hiệu Suất Và Sự Tiện Lợi

Quyết định chính mà tôi đã đưa ra và không hoàn toàn phù hợp với việc giữ cho bot gọn nhẹ là sử dụng MongoDB làm cơ sở dữ liệu. Dưới đây là kết quả từ lệnh top trên máy chủ tại thời điểm hiện tại, được sắp xếp theo mức sử dụng bộ nhớ:

MongoDB Memory Usage

MongoDB đang sử dụng hơn 3 gigabyte bộ nhớ thường trú, một lượng tương tự như chính tiến trình của bot. Công bằng mà nói, tôi có thể giảm điều đó bằng cách đặt --wiredTigerCacheSizeGB thấp hơn nhiều và bất kỳ sự suy giảm hiệu suất nào có thể là khá nhỏ. Vì bot có hơn 9 gigabyte bộ nhớ khả dụng và hơn 3 gigabyte hoàn toàn miễn phí, nên không có nhu cầu.

Tôi có thể đã sử dụng PostgreSQL thay vào đó và nó có thể đã cho tôi thêm một chút dung lượng RAM và nhiều dung lượng CPU hơn, nhưng động thái tiết kiệm tài nguyên thực sự sẽ là sử dụng SQLite, vốn đã đủ cho bot này. SQLite thường bị hiểu lầm là một cơ sở dữ liệu “đồ chơi”, và người ta còn thường cho rằng cái “đồ chơi” đó cũng có nghĩa là hiệu suất kém. Mặc dù đây là một chủ đề phức tạp, đó là một đánh giá nhìn chung không công bằng vì SQLite tránh được chi phí phát sinh từ:

  1. Giao tiếp giữa các tiến trình (điều này rất lớn)
  2. Kiểm soát truy cập
  3. Khóa cấp hàng (Row-level locking)
  4. …và nhiều hơn nữa

SQLite không có nhiều tính năng “khủng” này, điều này mang lại cho nó một hiệu suất tăng cường cho các mô hình truy cập ít đồng thời (điều này đúng với bot của tôi).

Tuy nhiên, tôi vẫn cảm thấy thoải mái với quyết định sử dụng Mongo khi nhìn lại. Tôi đã sử dụng nó như một cơ hội để học một cơ sở dữ liệu mới, nó phù hợp với instance hiện có, tôi thích các công cụ chính thức (Mongo Compass) và nó đưa tôi đến nơi tôi cần. Nhưng nó sẽ nằm trong danh sách cắt giảm nếu tôi cần lấy lại thêm bộ nhớ và dung lượng CPU.

Lời Kết

Vâng, đó là cách tôi làm điều đó! Đó là sự kết hợp của việc tận dụng các tùy chọn lưu trữ miễn phí tuyệt vời có sẵn, chọn các dependency nhanh chóng và thực hiện một vài tối ưu hóa có mục tiêu. Đừng ngần ngại bình luận nếu bạn có bất kỳ câu hỏi hoặc góp ý nào.

Chỉ mục