Chạy nhiều server khi gửi mail

Chào mọi người

Mình đang có một hệ thống gửi mail, dùng winservice (đang để trên server 1, cứ 3s chạy 1 lần) lấy dữ liệu trong database để gửi mail đến người nhận, sau khi gửi thì update trạng thái gửi từ 0 -> 1(Mỗi lần lấy 30 dòng, dùng Parallel để chạy).

Hiện tại hệ thống vẫn đang chạy tốt, tuy nhiên gần đây nhu cầu gửi mail tăng cao nên mình muốn clone cái service đó lên server 2 và server 3 (nếu sau này tăng trưởng mạnh hơn có thể sẽ clone ra 4 con server khác) để tăng performance của hệ thống.

Tuy nhiên nếu clone ra như vậy sẽ gặp trường hợp 1 con server bất kỳ nào vào lấy thông tin để gửi nhưng chưa kịp update trạng thái gửi về bằng 1 thì server khác đã vô lấy đúng thông tin row đó để gửi tiếp, dẫn đến 1 khách hàng nhận đến 2 mail

Hiện tại mình đang có 2 hướng xử lý như sau:

  1. Tạo thêm 1 colum flag, khi 1 server vô database lấy lên 30 dòng thì ngay lập tức update cột falg lên 1 để các server khác thấy và ko lấy các dòng có falg =1. Tuy nhiên mình sợ nếu sau này số lượng server tăng thì chưa kịp cập nhật flag =1 thì đã có server khác vô lấy dòng đấyđể gửi mail => vẫn bị trùng

  2. Vẫn tạo thêm cột flag (1,2,3 tương ứng với số server chạy service), nhưng là để đánh dấu cho từng con server, server nào thì chạy vào lấy đúng flag của server đó để gửi. Cách này mình thấy ổn, chạy độc lập ko dẫm chân nhau nhưng nếu server nào bị lỗi thì các dữ liệu thuộc flag của server đấy cũng sẽ ko đc xử lý

Nhờ mọi người nhận xét hoặc có giải pháp nào hay hơn góp ý giúp mình với nhé

Mình cám ơn ạ

3 Likes

Không gộp chung được, vậy ko scale được: Phải 2 service khác: emailservice., dataService.
Datasevice get data -> push queue -> call email service.
Trong trường hợp này muốn scale thì chỉ cần scale emailservice thôi.
Xem xet dùng actor model(aka lib) hoặc ringbuffer(LMAX) cho cái gửi email này. thay vì thêm server.(send email nên chạy async).

8 Likes

Về cơ bản, cậu nên làm theo cách của @nguyenhuuca đề cập. Gửi email có thể async được, và thao tác gửi email mới tốn kém, nên cậu có thể dùng message broker cùng với các email sending worker. Khi cậu đọc tử CSDL ra, đưa nội dung cần gửi vào message broker, rồi để các email sending worker pick từng message và gửi đi.
Khi đó, cậu chỉ cần scale email sending worker thôi (hay email service mà @nguyenhuuca đề cập).

Tuy nhiên, tớ tò mò chút, cậu đang dùng Cơ sở dữ liệu quan hệ phải không? Cậu hoàn toàn có thể giới thiệu thêm flag đánh dấu đã gửi hay chưa. Khi cập nhật flag, cậu cập nhật ở bản ghi có ID đã xác định và flag = false, rồi xem số lượng dòng update được.
Nếu số dòng trả về bằng 0, đồng nghĩa với việc đã có process khác update record đó rồi, nên cậu không cần làm gì nữa. Nếu số dòng trả về bằng 1, cậu triển thôi :smile:
Điều này chắc giải quyết giúp cậu rắc rối ở hướng #1 rồi.

Hope it helps!

6 Likes

Đầu tiên cám ơn bạn về câu trả lời,

Về bản chất mình vẫn đang là 2 service đó bạn ,1 cái dùng để lấy thông tin send mail, 1 cái dùng để gửi mail. Vì đặc thù yêu cầu hệ thống của công ty, khi scale lên sẽ cho scale ở trên một con server khác. Project của mình viết bằng .NET CORE nên 2 cái actor model(aka lib) hoặc ringbuffer(LMAX) bạn gợi ý có lẽ mình dùng ko đc

Công ty mình gửi mail khác nhiều, 1 ngày có khi gửi cả 100k mail nên mình dùng Parallel để tối ưu hóa tốc độ gửi mail. Ko biết vì sao gửi mail lại nên chạy async bạn nhỉ

Thằng này có cho net nhé.
Dùng Parallel tối ưu thì ko vân đề gi. Paralle mà update chung db thì mới sợ. vì khi update nó phải tuần tư.
Async nó giúp scale, không phụ thuộc vào vendor. giả sử gửi đến gmail mà gmail nó chết. sau 5s-10s nó mới trả timeout thì đợi mút mùa.
Còn có logic thi sau khi send, push ngược lại vào môt cái queue khác để xử lý. Tăng per thì mặc định send ok hết. chỉ xử lý khi fail thôi.

5 Likes

Cám ơn bạn,

Vấn đề ở #1 mình có nói rồi á, sợ chưa kịp cập nhật thì có một con server khác vào lấy thông tin đó đi gửi rồi => bị duplicate

Không biết trong 2 giải pháp trên, bạn cảm thấy giải pháp nào phù hợp hơn nhỉ

Cám ơn bạn,

Về actor model(aka lib) thì mình sẽ nghiên cứu sau, bạn cho mình hỏi trong 2 giải pháp trên thì bạn thấy giải pháp nào tối ưu hơn nhỉ

Nếu chọn 1-2 cách đó thì chọn cách 2.
Cách 2 gần giống với Consistent Hashing. dưa trên hash để phân bổ data cho phù hợp.

2 Likes

Ví dụ giá trị flag mặc định là 0
Thay vì đầu tiên là select flag = 0 thì đầu tiên chạy câu sql update (limit) những dòng có cột flag = 0 thành giá trị khác tùy theo server.
Sau đó mới chạy câu sql select lại những dòng có flag là giá trị mới và xử lý như bình thường

1 Like

Mình không hiểu làm sao có chuyện này xảy ra được. Lý do: không việc gì cho các server đọc/ cập nhật chung trên 1 table nếu ta không làm chủ được cơ chế phân bổ tài nguyên như những lập trình viên viết kernel. Nếu buộc phải chung table thì phải dùng thao tác locking trong Transactions để phòng ngừa chuyện như bạn đang thắc mắc. (Thêm cho vui: mình từng thắc mắc việc đang chép file mà bị ngắt điện hoặc rút USB ra thì dữ liệu rơi rớt ra mặt bàn hay không, thì mình đã thử rút ngay xem chuyện gì xảy ra, rồi rút dây mạng để thử ra làm sao, đang chạy ứng dụng CSDL thì kill nó… thực ra chẳng có chuyện quái gì nguy hiểm, mấy ông bên sàn chứng khoán và phóng tên lửa NASA còn chấp nhận sai sót được thì ta chẳng lo).

Nếu bạn có quyền lập trình mà đụng được đến CSDL thì bạn trích ra table cho từng server phụ trách gửi email riêng => thằng nào dùng table dữ liệu của chính mình thôi, không có ảnh hưởng gì thẳng khác, nó die thì tồn đọng đó, cuối ngày lại có một cái đi gom và gửi vét. Một cái table chung nếu là nguồn để nạp danh sách email liên tục theo thời gian thì sẽ có chức năng trích ra làm từng table riêng, làm cái như schedule trên server chứa dữ liệu phân các dòng email từ table tổng về cho từng table riêng.

4 Likes

Hình như cậu chưa biết, UPDATE command trong SQL trả về số dòng bị ảnh hưởng.
Nếu cậu viết câu UPDATE như thế này:

UPDATE flag = 1 FROM some_table WHERE id = <id> AND flag = 0;

Giả sử cậu chỉ có 1 bản ghi có id = <id>. Câu lệnh này sẽ trả về:

  • 0 nếu như bản ghi <id> có flag = 1 (tức không có dòng nào bị thay đổi bởi câu UPDATE).
  • 1 nếu như bản ghi <id> có flag = 0 (tức có 1 dòng bị thay đổi bởi câu UPDATE).

Trong ngữ cảnh concurrency, khi cậu có nhiều transaction update chung 1 bản ghi <id>, sẽ chỉ có 1 transaction nhận được kết quả 1. Các transaction còn lại sẽ nhận kết quả 0.

Áp dụng điều trên vào bài toán của cậu, cậu chỉ cần thực hiện:

  • Lấy 1 bản ghi có flag = 0 (bản ghi chưa được gửi)
  • Update flag = 1 cho bản ghi đó bằng câu UPDATE kể trên.
    • Nếu kết quả nhận về là 1 (a.k.a 1 bản ghi được update), cậu gửi email, và thậm chí có thể update flag = 2 sau khi đã gửi thành công. Nếu gửi thất bại, rollback flag về 0 (để xử lý lại), hoặc -1 nếu cậu muốn skip.
    • Nếu kết quả nhận về là 0 (a.k.a 0 có bản ghi nào được update, hay đã có ai đó đánh dấu gửi email cho bản ghi này rồi), cậu skip và thực hiện bản ghi tiếp theo.

Giải thuật đơn giản trên giúp cậu detect ngay lập tức việc ai đã gửi gì hay chưa, đảm bảo việc gửi email không bị duplicate, mà không phải implement lock nào phức tạp (thứ sẽ làm giảm throughput của hệ thống).

Không biết trong 2 giải pháp trên, bạn cảm thấy giải pháp nào phù hợp hơn nhỉ

Cậu đưa ra 2 giải pháp đều có vấn đề chưa được giải quyết (cách 1 cậu nói vẫn bị trùng, cách 2 thì không giải quyết được việc khi cậu service in-out, hay retry 1 server bị down, hay thậm chí logic phân phối các flag vào server + đánh số server như thế nào vẫn là câu hỏi lớn), vậy làm sao các “giải pháp” này có thể “phù hợp” nhỉ? :smile:

Cậu có thể đọc kỹ hướng dẫn phía trên của @nguyenhuuca (đó là cách tốt nhất, được sử dụng rộng rãi trong industry. Hiện tại, traffic volume của bên cậu chưa lớn - 100k email/ngày = average 1.1 email/s là lượng traffic rất nhỏ, và phương án dùng message broker để scale email sending worker hoàn toàn có thể giúp cậu xử lý lượng traffic lớn hơn thế nhiều chục lần. Cậu cũng không cần phải dùng akka, bất cứ message broker nào khác cũng được, như rabbit MQ chẳng hạn), hoặc hướng dẫn của tớ ở trên (cho cậu 1 phương án đơn giản, scale được, và nó dựa theo hướng #1 của cậu. Tất nhiên cậu vẫn cần đánh đổi một số điều). Đừng hỏi tớ, hay chỉ stick với việc so sánh 2 phương án của cậu, vì nó chưa được coi là giải pháp hoàn chỉnh đâu.

Hope it helps!

9 Likes

Cám ơn bạn vì sự nhiệt tình và chia sẻ kiến thức. Những điều mà mọi người chia sẻ đều rất có ích cho mình

Mình nghĩ mình sẽ tập trung vào message broker, mà điển hình ở đây là Kafka do công ty đã dựng sẵn môi trường cho Kafka rồi

1 Like
83% thành viên diễn đàn không hỏi bài tập, còn bạn thì sao?