RoadRunner: PHP không được xây dựng để chết, hay Golang để giải cứu

RoadRunner: PHP không được xây dựng để chết, hay Golang để giải cứu

Này Habr! Chúng tôi đang hoạt động tại Badoo làm việc trên hiệu suất PHP, vì chúng tôi có một hệ thống khá lớn bằng ngôn ngữ này và vấn đề hiệu suất là vấn đề tiết kiệm tiền. Hơn mười năm trước, chúng tôi đã tạo PHP-FPM cho việc này, lúc đầu là một tập hợp các bản vá cho PHP, sau đó được đưa vào bản phân phối chính thức.

Trong những năm gần đây, PHP đã đạt được những tiến bộ vượt bậc: trình thu gom rác đã được cải thiện, mức độ ổn định tăng lên - ngày nay bạn có thể viết daemon và các tập lệnh tồn tại lâu dài trong PHP mà không gặp bất kỳ sự cố nào. Điều này cho phép Spiral Scout tiến xa hơn: RoadRunner, không giống như PHP-FPM, không dọn sạch bộ nhớ giữa các yêu cầu, giúp tăng thêm hiệu suất (mặc dù cách tiếp cận này làm phức tạp quá trình phát triển). Chúng tôi hiện đang thử nghiệm công cụ này, nhưng chúng tôi chưa có bất kỳ kết quả nào để chia sẻ. Để làm cho việc chờ đợi họ vui hơn, chúng tôi xuất bản bản dịch của thông báo RoadRunner từ Spiral Scout.

Cách tiếp cận từ bài báo gần với chúng tôi: khi giải quyết các vấn đề của mình, chúng tôi cũng thường sử dụng một loạt PHP và Go, nhận được lợi ích của cả hai ngôn ngữ và không từ bỏ ngôn ngữ này để ủng hộ ngôn ngữ kia.

Chúc các bạn luôn vui vẻ!

Trong mười năm qua, chúng tôi đã tạo các ứng dụng cho các công ty từ danh sách Tài sản 500và dành cho các doanh nghiệp có đối tượng không quá 500 người dùng. Trong suốt thời gian qua, các kỹ sư của chúng tôi đã phát triển phần phụ trợ chủ yếu bằng PHP. Nhưng hai năm trước, một điều gì đó đã có tác động lớn không chỉ đến hiệu suất của các sản phẩm của chúng tôi mà còn đến khả năng mở rộng của chúng - chúng tôi đã giới thiệu Golang (Cờ vây) vào hệ thống công nghệ của mình.

Gần như ngay lập tức, chúng tôi phát hiện ra rằng Go cho phép chúng tôi xây dựng các ứng dụng lớn hơn với hiệu suất được cải thiện lên tới 40 lần. Với nó, chúng tôi có thể mở rộng các sản phẩm PHP hiện có của mình, cải thiện chúng bằng cách kết hợp các lợi ích của cả hai ngôn ngữ.

Chúng tôi sẽ cho bạn biết sự kết hợp giữa Go và PHP giúp giải quyết các vấn đề phát triển thực tế như thế nào và nó đã trở thành một công cụ giúp chúng tôi giải quyết một số vấn đề liên quan như thế nào. Mô hình chết PHP.

Môi trường phát triển PHP hàng ngày của bạn

Trước khi chúng ta nói về cách bạn có thể sử dụng Go để hồi sinh mô hình đang chết dần chết mòn của PHP, hãy xem qua môi trường phát triển PHP mặc định của bạn.

Trong hầu hết các trường hợp, bạn chạy ứng dụng của mình bằng cách sử dụng kết hợp máy chủ web nginx và máy chủ PHP-FPM. Cái trước phục vụ các tệp tĩnh và chuyển hướng các yêu cầu cụ thể tới PHP-FPM, trong khi bản thân PHP-FPM thực thi mã PHP. Bạn có thể đang sử dụng kết hợp ít phổ biến hơn của Apache và mod_php. Nhưng mặc dù nó hoạt động hơi khác một chút, nhưng các nguyên tắc đều giống nhau.

Chúng ta hãy xem cách PHP-FPM thực thi mã ứng dụng. Khi có một yêu cầu, PHP-FPM sẽ khởi tạo một tiến trình con PHP và chuyển các chi tiết của yêu cầu như một phần trạng thái của nó (_GET, _POST, _SERVER, v.v.).

Trạng thái không thể thay đổi trong quá trình thực thi tập lệnh PHP, vì vậy cách duy nhất để có được một bộ dữ liệu đầu vào mới là xóa bộ nhớ tiến trình và khởi tạo lại.

Mô hình thực thi này có nhiều ưu điểm. Bạn không phải lo lắng quá nhiều về mức tiêu thụ bộ nhớ, tất cả các quy trình đều được cách ly hoàn toàn và nếu một trong số chúng "chết" thì nó sẽ tự động được tạo lại và không ảnh hưởng đến các quy trình còn lại. Nhưng cách tiếp cận này cũng có những nhược điểm xuất hiện khi cố gắng mở rộng ứng dụng.

Nhược điểm và sự kém hiệu quả của môi trường PHP thông thường

Nếu bạn là một nhà phát triển PHP chuyên nghiệp, thì bạn biết nơi bắt đầu một dự án mới - với việc lựa chọn một khung. Nó bao gồm các thư viện tiêm phụ thuộc, ORM, bản dịch và mẫu. Và, tất nhiên, tất cả đầu vào của người dùng có thể được đưa vào một đối tượng một cách thuận tiện (Symfony/HttpFoundation hoặc PSR-7). Các khung thật tuyệt!

Nhưng cái gì cũng có giá của nó. Trong bất kỳ khung cấp doanh nghiệp nào, để xử lý một yêu cầu đơn giản của người dùng hoặc quyền truy cập vào cơ sở dữ liệu, bạn sẽ phải tải ít nhất hàng tá tệp, tạo nhiều lớp và phân tích cú pháp một số cấu hình. Nhưng điều tồi tệ nhất là sau khi hoàn thành mỗi nhiệm vụ, bạn sẽ phải đặt lại mọi thứ và bắt đầu lại: tất cả mã bạn vừa khởi tạo trở nên vô dụng, với sự trợ giúp của nó, bạn sẽ không thể xử lý yêu cầu khác được nữa. Hãy nói điều này với bất kỳ lập trình viên nào viết bằng một số ngôn ngữ khác, và bạn sẽ thấy sự hoang mang trên khuôn mặt anh ta.

Các kỹ sư PHP đã tìm cách giải quyết vấn đề này trong nhiều năm, sử dụng các kỹ thuật tải chậm thông minh, vi khung, thư viện được tối ưu hóa, bộ đệm, v.v. Nhưng cuối cùng, bạn vẫn phải đặt lại toàn bộ ứng dụng và bắt đầu lại nhiều lần. (Ghi chú của người dịch: vấn đề này sẽ được giải quyết một phần với sự ra đời của preload trong PHP 7.4)

PHP với Go có thể tồn tại nhiều hơn một yêu cầu không?

Có thể viết các tập lệnh PHP tồn tại lâu hơn vài phút (lên đến hàng giờ hoặc hàng ngày): ví dụ: tác vụ định kỳ, trình phân tích cú pháp CSV, bộ ngắt hàng đợi. Tất cả chúng đều hoạt động theo cùng một kịch bản: chúng truy xuất một tác vụ, thực hiện nó và đợi tác vụ tiếp theo. Mã luôn nằm trong bộ nhớ, tiết kiệm được một phần nghìn giây quý giá vì có nhiều bước bổ sung cần thiết để tải khung và ứng dụng.

Nhưng việc phát triển các tập lệnh tồn tại lâu dài không hề dễ dàng. Bất kỳ lỗi nào cũng giết chết hoàn toàn quy trình, chẩn đoán rò rỉ bộ nhớ đang gây khó chịu và việc gỡ lỗi F5 không còn khả thi nữa.

Tình hình đã được cải thiện với việc phát hành PHP 7: một trình thu gom rác đáng tin cậy đã xuất hiện, việc xử lý lỗi trở nên dễ dàng hơn và các phần mở rộng kernel hiện không bị rò rỉ. Đúng vậy, các kỹ sư vẫn cần cẩn thận với bộ nhớ và lưu ý các vấn đề về trạng thái trong mã (có ngôn ngữ nào có thể bỏ qua những điều này không?). Tuy nhiên, PHP 7 có ít bất ngờ hơn dành cho chúng ta.

Có thể lấy mô hình làm việc với các tập lệnh PHP tồn tại lâu dài, điều chỉnh nó cho các tác vụ tầm thường hơn như xử lý các yêu cầu HTTP và do đó loại bỏ nhu cầu tải mọi thứ từ đầu với mỗi yêu cầu không?

Để giải quyết vấn đề này, trước tiên chúng tôi cần triển khai một ứng dụng máy chủ có thể chấp nhận các yêu cầu HTTP và chuyển hướng từng yêu cầu một đến PHP worker mà không giết chết nó mỗi lần.

Chúng tôi biết rằng chúng tôi có thể viết một máy chủ web bằng PHP thuần túy (PHP-PM) hoặc sử dụng phần mở rộng C (Swoole). Và mặc dù mỗi phương pháp đều có những ưu điểm riêng, nhưng cả hai lựa chọn đều không phù hợp với chúng tôi - chúng tôi muốn một thứ gì đó hơn thế nữa. Chúng tôi cần nhiều thứ hơn là chỉ một máy chủ web - chúng tôi mong đợi có được một giải pháp có thể cứu chúng tôi khỏi các vấn đề liên quan đến “khởi đầu khó khăn” trong PHP, đồng thời có thể dễ dàng điều chỉnh và mở rộng cho các ứng dụng cụ thể. Đó là, chúng tôi cần một máy chủ ứng dụng.

Go có thể giúp với điều này? Chúng tôi biết điều đó có thể bởi vì ngôn ngữ biên dịch các ứng dụng thành các tệp nhị phân đơn lẻ; nó là nền tảng chéo; sử dụng mô hình xử lý song song (đồng thời) của riêng nó, rất thanh lịch và một thư viện để làm việc với HTTP; và cuối cùng, hàng nghìn thư viện nguồn mở và tích hợp sẽ có sẵn cho chúng tôi.

Những khó khăn khi kết hợp hai ngôn ngữ lập trình

Trước hết, cần xác định cách hai hoặc nhiều ứng dụng sẽ giao tiếp với nhau.

Ví dụ, sử dụng thư viện xuất sắc Alex Palaestras, có thể chia sẻ bộ nhớ giữa các quy trình PHP và Go (tương tự như mod_php trong Apache). Nhưng thư viện này có các tính năng hạn chế việc sử dụng nó để giải quyết vấn đề của chúng tôi.

Chúng tôi quyết định sử dụng một cách tiếp cận khác, phổ biến hơn: xây dựng sự tương tác giữa các quy trình thông qua ổ cắm/đường ống dẫn. Cách tiếp cận này đã được chứng minh là đáng tin cậy trong nhiều thập kỷ qua và đã được tối ưu hóa tốt ở cấp độ hệ điều hành.

Để bắt đầu, chúng tôi đã tạo một giao thức nhị phân đơn giản để trao đổi dữ liệu giữa các quy trình và xử lý lỗi truyền. Ở dạng đơn giản nhất, loại giao thức này tương tự như dây lưới с tiêu đề gói kích thước cố định (trong trường hợp của chúng tôi là 17 byte), chứa thông tin về loại gói, kích thước của gói và mặt nạ nhị phân để kiểm tra tính toàn vẹn của dữ liệu.

Về phía PHP, chúng tôi đã sử dụng chức năng gói, và ở bên Đi, thư viện mã hóa / nhị phân.

Đối với chúng tôi, dường như một giao thức là không đủ - và chúng tôi đã thêm khả năng gọi net/rpc go dịch vụ trực tiếp từ PHP. Sau này, điều này đã giúp chúng tôi rất nhiều trong quá trình phát triển, vì chúng tôi có thể dễ dàng tích hợp các thư viện Go vào các ứng dụng PHP. Ví dụ, kết quả của công việc này có thể được nhìn thấy trong sản phẩm mã nguồn mở khác của chúng tôi goridge.

Phân phối nhiệm vụ trên nhiều công nhân PHP

Sau khi triển khai cơ chế tương tác, chúng tôi bắt đầu nghĩ về cách hiệu quả nhất để chuyển các tác vụ sang các quy trình PHP. Khi một nhiệm vụ đến, máy chủ ứng dụng phải chọn một nhân viên miễn phí để thực hiện nó. Nếu một công nhân/quy trình thoát với lỗi hoặc "chết", chúng tôi sẽ loại bỏ nó và tạo một quy trình mới để thay thế nó. Và nếu công nhân/quy trình đã hoàn thành thành công, chúng tôi sẽ trả lại nó cho nhóm công nhân có sẵn để thực hiện các nhiệm vụ.

RoadRunner: PHP không được xây dựng để chết, hay Golang để giải cứu

Để lưu trữ nhóm công nhân đang hoạt động, chúng tôi đã sử dụng kênh đệm, để loại bỏ các nhân viên "chết" bất ngờ khỏi nhóm, chúng tôi đã thêm cơ chế theo dõi lỗi và trạng thái của nhân viên.

Kết quả là chúng tôi có một máy chủ PHP đang hoạt động có khả năng xử lý bất kỳ yêu cầu nào được trình bày ở dạng nhị phân.

Để ứng dụng của chúng tôi bắt đầu hoạt động như một máy chủ web, chúng tôi phải chọn một tiêu chuẩn PHP đáng tin cậy để đại diện cho mọi yêu cầu HTTP đến. Trong trường hợp của chúng tôi, chúng tôi chỉ biến đổi yêu cầu net/http từ Chuyển đến định dạng PSR-7để nó tương thích với hầu hết các framework PHP hiện nay.

Bởi vì PSR-7 được coi là bất biến (một số người sẽ nói về mặt kỹ thuật là không), các nhà phát triển phải viết các ứng dụng không coi yêu cầu là một thực thể toàn cầu về nguyên tắc. Điều này rất phù hợp với khái niệm về các quy trình PHP tồn tại lâu dài. Phần triển khai cuối cùng của chúng tôi, vẫn chưa được đặt tên, trông như thế này:

RoadRunner: PHP không được xây dựng để chết, hay Golang để giải cứu

Giới thiệu RoadRunner - máy chủ ứng dụng PHP hiệu suất cao

Nhiệm vụ thử nghiệm đầu tiên của chúng tôi là một chương trình phụ trợ API, nhiệm vụ này sẽ bùng nổ theo định kỳ một cách khó đoán (thường xuyên hơn nhiều so với bình thường). Mặc dù nginx là đủ trong hầu hết các trường hợp, nhưng chúng tôi thường xuyên gặp phải lỗi 502 vì chúng tôi không thể cân bằng hệ thống đủ nhanh để đáp ứng mức tăng tải dự kiến.

Để thay thế giải pháp này, chúng tôi đã triển khai máy chủ ứng dụng PHP/Go đầu tiên của mình vào đầu năm 2018. Và ngay lập tức có một hiệu ứng đáng kinh ngạc! Chúng tôi không chỉ loại bỏ hoàn toàn lỗi 502 mà còn có thể giảm XNUMX/XNUMX số lượng máy chủ, tiết kiệm rất nhiều tiền và thuốc đau đầu cho các kỹ sư và giám đốc sản phẩm.

Đến giữa năm, chúng tôi đã cải thiện giải pháp của mình, xuất bản nó trên GitHub theo giấy phép của MIT và đặt tên cho nó là RoadRunner, do đó nhấn mạnh tốc độ và hiệu quả đáng kinh ngạc của nó.

RoadRunner có thể cải thiện ngăn xếp phát triển của bạn như thế nào

ứng dụng RoadRunner cho phép chúng tôi sử dụng Middleware net/http trên Go side để thực hiện xác minh JWT trước khi yêu cầu đến PHP, cũng như xử lý WebSockets và trạng thái tổng hợp trên toàn cầu trong Prometheus.

Nhờ có RPC tích hợp, bạn có thể mở API của bất kỳ thư viện Go nào dành cho PHP mà không cần viết trình bao bọc tiện ích mở rộng. Quan trọng hơn, với RoadRunner, bạn có thể triển khai các máy chủ không phải HTTP mới. Các ví dụ bao gồm các trình xử lý đang chạy trong PHP AWS Lambda, tạo bộ ngắt hàng đợi đáng tin cậy và thậm chí thêm gRPC cho các ứng dụng của chúng tôi.

Với sự trợ giúp của cộng đồng PHP và Go, chúng tôi đã cải thiện tính ổn định của giải pháp, tăng hiệu suất ứng dụng lên tới 40 lần trong một số thử nghiệm, cải tiến các công cụ sửa lỗi, triển khai tích hợp với khung Symfony và thêm hỗ trợ cho HTTPS, HTTP/2, plugin và PSR-17.

Kết luận

Một số người vẫn bị cuốn vào quan niệm lỗi thời rằng PHP là một ngôn ngữ chậm, khó sử dụng, chỉ tốt cho việc viết plugin cho WordPress. Những người này thậm chí có thể nói rằng PHP có một hạn chế như vậy: khi ứng dụng đủ lớn, bạn phải chọn một ngôn ngữ “trưởng thành” hơn và viết lại cơ sở mã được tích lũy trong nhiều năm.

Đối với tất cả những điều này, tôi muốn trả lời: hãy suy nghĩ lại. Chúng tôi tin rằng chỉ có bạn đặt bất kỳ hạn chế nào cho PHP. Bạn có thể dành cả đời để chuyển đổi từ ngôn ngữ này sang ngôn ngữ khác, cố gắng tìm ngôn ngữ phù hợp nhất cho nhu cầu của mình hoặc bạn có thể bắt đầu coi ngôn ngữ là công cụ. Những sai sót được cho là của một ngôn ngữ như PHP thực sự có thể là lý do cho sự thành công của nó. Và nếu bạn kết hợp nó với một ngôn ngữ khác như Go, thì bạn sẽ tạo ra những sản phẩm mạnh mẽ hơn nhiều so với việc bạn bị giới hạn sử dụng bất kỳ ngôn ngữ nào.

Đã từng làm việc với nhiều Go và PHP, có thể nói rằng chúng tôi yêu thích chúng. Chúng tôi không có kế hoạch hy sinh cái này để lấy cái kia - ngược lại, chúng tôi sẽ tìm cách để nhận được nhiều giá trị hơn nữa từ ngăn xếp kép này.

CẬP NHẬT: chúng tôi hoan nghênh tác giả của RoadRunner và đồng tác giả của bài báo gốc - dây buộc

Nguồn: www.habr.com

Thêm một lời nhận xét