Hiện nay, trong giới kinh doanh người ta có một quy luật bất thành văn: “cứ thoái mái build MVP trước, tính ổn định tính sau”. Và như vậy, người ta cứ đăm đăm đẩy ra hết sản phẩm này đến sản phẩm khác. Miễn sao mối làm ăn của bạn hiện “ăn nên làm ra”, thì chả cần phải quan tâm đến vấn đề mở rộng làm gì cả; chỉ cần lo kiểm nghiệm ý tưởng, đánh giá thị trường và thu hút sự chú ý là quá đủ. Khả năng mở rộng để sau hẵng tính. Thật không may, quan niệm sai lầm này đã đẩy nhiều ý tưởng đầy tiềm năng đến thất bại ê chề. Cứ nhìn lại vụ Pokémon GO mà xem.

Jonathan Zarra, người tạo ra GoChat cho Pokémon GO, hiển nhiên sẽ không có gan tái phạm sai lầm này lần thứ hai. Tuần rồi, khi trò chuyện với VCs, siêu sao với ứng dụng chat thu hút 1 triệu người dùng trong 5 ngày vừa cho biết anh phát triển và thương mại hóa ứng dụng như thế nào. Giờ thì GoChat biến mất tăm mất tích, đi cùng với đó là hơn 1 triệu người dùng cùng với một đống chi phí ngớ ngẩn, thật đáng tiếc cho một ý tưởng thiên tài.

Bài viết chỉ ra rằng Zarra đã rất chật vật khi phải quản lý nhiều server phục vụ cho hơn 1 triệu active user. Ứng dụng ban đầu chỉ là MVP thôi, còn chuyện mở rộng là chuyện tương lai, vì chính anh cũng đâu ngờ ứng dụng lại “nổi” như thế. Bên nhà thầu cho hay server ngốn khoảng 4.000 đô. Tôi cho rằng 4.000 đô này không chỉ là phần cứng, nhưng là 4.000 đô chi phí cho virtual server và traffic hằng năm hay hằng tháng đây?

Có thể nói, trong suốt sự nghiệp, tôi đã từng vô số lần thiết kế và xây dựng web platforms cho hàng trăm triệu active user. Và thậm chí với MVP, con số 4.000 đô thực sự quá dư thừa cho một chat app 1 triệu user. Việc build một hệ thống (cho hàng triệu người dùng hằng tháng) tiếp kiệm và mở rộng được không dễ. Nhưng việc setup một vài server rẻ tiền trên cloud cho số người dùng nhất định cũng đâu khó khăn gì. Bạn chỉ cần chú ý một chút với MVP của mình, và một vài lựa chọn đúng đắn, có lẽ tình huống sẽ được cứu rỗi phần nào.

GoSnaps: 500,000 user, 5 ngày, server mất $100/tháng

Tương tự GoChat, tôi cũng vừa ra mắt một fan app tên GoSnaps cho Pokémon GO hồi tuần trước. GoSnap là một ứng dụng cho phép chia sẻ screenshots và hình ảnh trong Pokémon GO lên một bản đồ. Nói cách khác, Instagram/Snapchat cho Pokémon GO. Ngày đầu tiên GoSnap cán mốc 60k user, 160k user ở ngày thứ hai và hơn 500k unique user và gần 200k lượt snap sau năm ngày ra mắt. Ứng dụng vô cùng đơn giản, phần mềm sẽ tự động nhận dạng xem thử hình ảnh được upload có liên quan đến Pokémon GO hay không, thêm với một công cụ resize để up ảnh nữa. Với “combo” trên, chúng tôi chỉ dùng một Google Cloud server tầm trung giá 100 đô/tháng, cùng với dịch vụ Google Cloud Storage (giá rẻ) để lưu trữ hình ảnh. Hiệu suất đến nay vẫn ổn, mà chỉ mất 100 đô/tháng.

GoChat và GoSnaps trên bàn cân kỹ thuật

Hãy thử so sánh GoChat và GoSnap. Trong mỗi khu vực nhất định của bản đồ, có lẽ cả hai ứng dụng đều bắn rất nhiều request mỗi giây để đẩy chats/images đi. Nói cách khác, đây là thao tác tìm kiếm địa lý trong database (hoặc search engine), thông qua phép đo polygon (đa giác) của vị trí theo kinh/vĩ độ, hoặc qua một điểm cụ thể. Chúng tôi lựa chọn polygon và bắn request mỗi khi có người di chuyển bản đồ. Những kiểu queries như thế này có sức nặng rất lớn lên database, đặc biệt khi phải kết hợp thêm với shorting hay filtering. Chúng tôi, và có lẽ là GoChat nữa, thường nhận đến hàng trăm request kiểu này mỗi giây.

Đặc biệt với GoChat, ứng dụng phải gửi và post hàng đống chat message mỗi giây. Ở bài trước có đề cập, GoChat nhận khoảng 600 request mỗi giây cho cả ứng dụng. 600 request này là tổng cả map request và chat message. Những chat message này khá nhỏ gọn; có thể/cần được thực hiện trên một socket connection đơn giản, nhưng lại xuất hiện rất thường xuyên và cần được phân bố đến nhiều chatter khác. Với một cấu trúc hợp lý, ta hoàn toàn có khả năng quản lý được, nhưng với một cấu trúc MVP nghèo nàn thì… chúc may mắn.

GoSnap thì khác hẳn, có một lượng lớn hình ảnh và lượt “like” được gửi đi mỗi giây. Những snap này sẽ đần chất đống trên server, vì snap cũ cũng được dùng tới, còn chat cũ thường thì không. Với một lập trình viên như tôi, vì hình ảnh được lưu trữ trên Google Cloud Storage, nên các file ảnh được request không phải là mối bận tâm quá lớn. Google Cloud sẽ xử lý vấn đề này, và tôi tin tưởng Google. Hơn nữa, GoSnap có bộ nhận diện lọc ra các hình ảnh không liên quan đến Pokémon GO, hình ảnh còn được resize trước khi gửi lên Cloud. Những thao tác này ngốn rất nhiều CPU và băng thông. Nặng hơn nếu so với mấy cái chat message bé tý, nhưng lại ở tuần suất thấp hơn. Kết luận, về mặt độ phức tạp khi mở rộng, cả hai ứng dụng khá giống nhau. GoChat xử lý nhiều mảnh message nhỏ, trong khi GoSnaps thực hiện các thao tác nặng nề. Cả hai tuy cần cách mở rộng khác nhau, nhưng cùng độ phức tạp.

Làm sao build được một MVP mở rộng được trong vòng 24h?

GoSnap cũng không khác GoChat là mấy, cũng chỉ là MVP được “nặn” nhanh trong 24h, chứ nào phải sản phẩm chuyên nghiệp. Đây chỉ là một project boilerplate trên NodeJS, chỉ dùng đến mỗi MongoDB database, không có luôn cả caching. Không Redis, không Varnish, không Nginx setting cầu kỳ, không có gì hết. Còn ứng dụng trên iOS, thực tế là một mớ native Objective-C code, thêm mấy đoạn code xoay quanh Apple Map vay mượn từ Unboxd. Vậy tôi mở rộng thế quái nào đây? Bằng cách “siêng” thêm một chút chứ sao.

Theo tôi, MVP không khác gì cuộc đua với thời gian, sao cho có được ứng dụng trong thời gian ngắn nhất, chả cần biết back-end như thế nào. Với kiểu “mỳ ăn liền” như vậy, tôi sẽ chứa hình ảnh của mình ở đâu? Tất nhiên là ở Database: MongoDB. Không cần cấu hình và chả cần phải code. Đơn giản thôi, MVP mà. Vậy tôi truy vấn snaps được nhiều like nhất ở một khu vực như thế nào? Tôi chỉ việc chạy MongoDB query trên toàn bộ snaps được upload. Một database collection chỉ cần một querry duy nhất. MVP thôi mà. Cứ tiếp tục thiết đặt kiểu “chơi chơi” này và ứng dụng của bạn sẽ sớm đi tong.

Hãy xem thử query mà đáng lẽ tôi phải chạy để nhận các snaps này:”tìm toàn bộ snaps trong đa giác [A, B, C, D], bỏ các snaps bị mark abuse ra, bỏ các snaps đang process ra, sắp xếp theo lượt like, sắp xếp các snap của Pokémon GO hợp lệ lên trước”. Với dataset nhỏ là hoàn hảo luôn. Tuy nhiên, với các load dữ liệu quá lớn, đây là một phương pháp thảm họa. Ngay cả khi bạn đơn giản hóa query bên trên xuống chỉ còn ba thao tác điều kiện/sắp xếp thôi, cách này vẫn dở như cũ. Vì sao ư? Vì database không phải được dùng theo kiểu này! Tại một thời điểm, một Database chỉ nên query 1 index thôi, nên nếu cứ chạy query theo kiểu này thì coi như xong. Nếu bạn không có nhiều người dùng thì không sao, nhưng khi mọi người đổ dồn vào ứng dụng thì số phận của bạn cũng không khác gì GoChat cả.

Vậy nếu không thể theo hướng đó, tôi cần phải làm gì đây? Sau khi dùng phần nhận diện ảnh và thực hiện resize vô cùng ngốn CPU, tôi sẽ đẩy hết những hình ảnh đã resize lên Google Cloud Storage. Như vậy, server và database của tôi chả phải lo bị đống request hình ảnh điên cuồng đánh sập. Database chỉ nên lo phần dữ liệu thôi, chứ không phải vụ hình ảnh. Bạn thấy đấy, cách này “cứu vớt” server được không ít. Mặt khác, tôi còn chia snaps thành nhiều nhóm khác nhau: tất cả snaps, snaps được like nhiều nhất, snaps mới nhất, snaps đẹp mới nhất,… Mỗi khi có snap mới được đăng tải (dù có được like hay bị gắn abuse đi nữa), code sẽ check thử xem snap này có (vẫn) thuộc vào các nhóm đã chia hay không và thao tác cho phù hợp. Với cách thiết lập này, code chỉ việc query theo các nhóm đã sắp sẵn mà không phải bù đầu bù cổ chạy mấy query phức tạp trên một “đống bầy nhầy”. Chỉ là chia data một cách logic thành nhiều bucket đơn giản hơn thôi mà, có gì khó đâu. Đến cùng, thay vì chạy thêm query phức tạp; tôi chỉ việc query tọa độ địa lý thôi, thêm một sorting operation đơn giản là xong. Kết luận: Hãy tìm cách sao cho việc lựa chọn data thật rõ ràng.

Vậy, cách làm này tốn bao nhiêu lâu? Chắc chỉ 2 đến 3 tiếng đồng hồ. Vậy khi không lại tốn thêm thời gian làm gì? Đây đơn giản chỉ là thói quen của bản thân tôi. Tôi luôn cho rằng ứng dụng của mình sẽ thành công (còn không nghĩ nó thành công thì ngồi build làm gì). Nếu ứng dụng của tôi gặp vấn đề và chết trong trứng nước do kỹ thuật kém, tôi không cách nào ngủ ngon giấc được. Tôi luôn thêm thắt một chút “đường” mở rộng vào ứng dụng của mình, ranh giới sống chết ở ngay đó đấy. Và tôi cho rằng MVP nào cũng nên dành ít thời gian một chút cho những vấn đề này.

Hãy chọn công cụ thích hợp cho MVP của bạn

Nếu GoSnap được viết bằng một thứ ngôn ngữ chậm chạp hay framework cồng kềnh, có khi tôi sẽ cần nhiều server hơn hiện nay. Hay nếu tôi dùng những ngôn ngữ đại loại như PHP với Symfony, hay Python với Django, hay Ruby trên Rails, chắc giờ này tôi còn đang ngồi cặm cụi fix mấy phần chạy chậm, hay ngồi thêm server. Tim tôi đi, “tai nạn” kiểu này tôi không chỉ gặp một lần. Những ngôn ngữ và framework này hiển nhiên có cái hay riêng, nhưng lại không hề phù hợp cho những MVP thiếu ngân sách chi cho server. Lý do chủ yếu là vì nhiều lớp code thường được dành để map các database record đến logic và framework code không cần thiết; từ đó “đốt” CPU như “lửa đốt xăng”. Để thấy được tầm quan trọng của vấn đề, ta hãy xét thử ví dụ sau.

Như đã nói ở trên, GoSnap dùng NodeJS làm ngôn ngữ/nền tảng backend, vừa nhanh vừa hiệu quả. Thêm đó, tôi dùng Mongoose làm ORM để giúp MongoDB vận hành dễ dàng hơn. Vì code base của Mongoose rộng “tràn giang đại hải”, mà tôi cũng chả phải chuyên gia Mongoose gì. Nên đáng lẽ phải tránh xa Mongoose ngay từ đầu (ai quan tâm chứ, MVP mà). Cuối tuần trước, 4 process NodeJS trên server của tôi có lúc đạt mức 90%CPU (cho mỗi process), chỉ với mức 800–1000 người dùng đồng thời, không thể chấp nhận được. Cuối cùng, tôi chỉ việc kích hoạt hàm “lean()” để có JSON object đơn giản, thay cho mớ Mongoose object “màu nhiệm”. Và NodeJS process liền hạ xuống mức 5–10% CPU. Như vậy, việc nắm bắt các vận hành (dù rất đơn giản) của code rất quan trọng, giảm load được dến 90% đấy! Cứ tưởng tượng nếu ta dùng một thư viện thật năng nề như Symfony với Doctrine mà xem. Ứng dụng phải cần một mớ server với thật nhiều lõi CPU chỉ để thực thi code, trong khi database mới nên bị “cổ lọ”, chứ không nên đến lượt code.

Để có một ứng dụng mở rộng được, việc lựa chọn ngôn ngữ nhanh gọn rất quan trọng (trừ khi bạn có dư tiền thuê server). Và một ngôn ngữ với thư viện hữu dụng còn quan trọng hơn nữa, vì bạn muốn nhanh có được MVP. NodeJS, Scala và Go là những ngôn ngữ đáp ứng được hết những yêu cầu này, với nhiều công cụ mang lại hiệu năng khá cao. Những ngôn ngữ như PHP hay Java chưa hẳn đã chậm, nhưng lại đi kèm với framework và codebase cồng kềnh làm ứng dụng nặng nề hơn. Những ngôn ngữ này cực kỳ phù hợp khi phát triển các ứng dụng có hướng phát triển rõ ràng và code đã qua test kỹ lưỡng, nhưng lại không hợp với mong muốn mở rộng, tiếp kiệm và nhanh chóng của đa số “MVPers” chúng ta. Để khỏi dấy lên tranh cãi “đáng tiếc”, tôi sẽ chốt luôn, đây chỉ là ý kiến chủ quan và phiến diện mà thôi.

MVP và tính mở rộng cũng có thể chung sống

Nếu ứng dụng của bạn có cơ hội phát triển mạnh mẽ, hãy luôn nhớ thêm khả năng mở rộng vào MVP của mình. Trái với quan niệm chung, MVP hoàn toàn có thể mở rộng thêm được. Không có gì buồn hơn khi bỏ công sức làm ra một ứng dụng thành công rồi lại ngậm ngùi nhìn nó thất bại chỉ vì vấn đề kỹ thuật. Bản thân Pokémon vẫn tồn đọng rất nhiều vấn đề, nhưng vì quá nổi nên cũng chả ai để ý đến. Startups nhỏ yếu làm gì có “vinh dự” này. “Thiên thời địa lợi” là vô cùng quan trọng, chắc hẳn một triêu người dùng GoChat và nửa triệu người dùng GoSnaps chắc cũng đồng tình với tôi.

Posted on Techtalk

Advertisements