Узнайте, как мы использовали Go вместе с Laravel, применяя коммуникацию через Unix-сокеты RPC для выполнения операций, критичных к производительности, и защиты проприетарных алгоритмов через скомпилированные бинарные файлы.
В последнее время я много работаю с полиглотными архитектурами, в частности, сочетая Laravel с сервисами на Go. Это комбинация работает невероятно хорошо, так как позволяет нам использовать богатую экосистему Laravel, одновременно получая выгоду от высокой производительности и преимуществ компиляции Go.
Давайте погрузимся в то, как мы структурировали этот гибридный подход, и рассмотрим некоторые интересные вызовы, которые нам удалось решить по пути…
Зачем смешивать Laravel и Go?
Когда вы работаете над PHP-приложением, которое выполняет ресурсоемкие вычислительные операции или обрабатывает огромные объемы данных, может наступить момент, когда вам нужно изучить другие варианты для удовлетворения требований к производительности. Это особенно актуально для зрелых приложений, которые значительно масштабировались со временем и теперь обрабатывают миллионы записей или обслуживают тысячи одновременных пользователей. Для нас было два основных фактора:
- Операции, критичные к производительности : Некоторые вычислительно сложные операции просто работают быстрее на Go. Хотя Laravel отлично подходит для создания функционально насыщенных приложений, характеристики производительности Go делают его лучшим выбором для определенных видов тяжелых задач. Например, для Geocodio часть этой нагрузки включает обработку миллиардов запросов — мы обрабатываем в среднем 17 миллиардов запросов каждые 30 дней.
- Скомпилированные бинарные файлы : Когда вам нужно распространять свое приложение для локальной установки, возможность предоставлять предварительно скомпилированные бинарные файлы становится крайне ценной. Среди прочего, это помогает сохранить ваши проприетарные алгоритмы и бизнес-логику в безопасности. Go делает это довольно просто.
Слой коммуникации
Одна из самых интересных частей нашей настройки — это то, как мы организовали коммуникацию между Laravel и Go. Вместо использования HTTP или команд оболочки, мы выбрали коммуникацию через Unix-сокеты с использованием RPC. Это дает нам почти нативную производительность для межпроцессного взаимодействия.
Вот как мы настроили сторону Laravel:
class GoService { private static ?RPC $rpc = null; private const SOCKET_PATH = '/run/app/service.sock'; public function __construct() { if (!self::$rpc) { self::$rpc = new RPC( Relay::create('unix://'.self::SOCKET_PATH) ); } } public function call(string $method, $args, $defaultValue = null) { try { $fullMethod = 'Service.'.$method; return self::$rpc->call($fullMethod, $args) ?? $defaultValue; } catch (Exception $e) { Log::error('Go service call failed', [ 'method' => $method, 'error' => $e->getMessage() ]); return $defaultValue; } } }
Затем мы можем обернуть конкретную функциональность в специализированные классы:
class ComputeService { private $service; public function __construct(GoService $service) { $this->service = $service; } public function processData(array $data): array { return $this->service->call('processData', $data, []); } }
На стороне Go мы настраиваем RPC-сервер, который прослушивает Unix-сокет:
type Service struct{} func (s *Service) ProcessData(args []interface{}, result *[]interface{}) error { // Обработка данных здесь return nil } func main() { service := new(Service) listener, err := net.Listen("unix", "/run/app/service.sock") if err != nil { log.Fatal(err) } // Убедитесь, что права доступа правильные err = os.Chmod("/run/app/service.sock", 0777) if err != nil { log.Fatal(err) } // Обработка завершения работы c := make(chan os.Signal) signal.Notify(c, os.Interrupt, syscall.SIGTERM) go func() { <-c listener.Close() os.Exit(0) }() rpc.Register(service) for { conn, err := listener.Accept() if err != nil { continue } go rpc.ServeCodec(goridge.NewCodec(conn)) } }
Обработка ошибок и надежность
Одной из проблем с этой архитектурой является грамотная обработка случаев отказа, что особенно важно для приложения, работающего на клиентских установках. Если возникает проблема с подключением между Laravel и Go или если сервис на Go падает, мы должны гарантировать, что основное приложение продолжает функционировать, пытаясь восстановить соединение. Для локальных развертываний эти сбои должны обрабатываться автоматически, поскольку нет DevOps-команды, которая могла бы вмешаться. Вот как мы подходим к этому:
class GoService { private $retries = 3; private $backoff = 100; // ms public function callWithRetry(string $method, $args) { $attempt = 0; while ($attempt < $this->retries) { try { return $this->call($method, $args); } catch (ConnectionException $e) { $attempt++; if ($attempt === $this->retries) { throw $e; } usleep($this->backoff * 1000 * $attempt); } } } }
Преимущества производительности
Коммуникация через Unix-сокеты добавляет минимальные накладные расходы — мы говорим о микросекундах. Это означает, что мы можем полностью использовать характеристики производительности Go. В нашем рабочем окружении мы наблюдали, что операции, которые занимали сотни миллисекунд в PHP, завершаются менее чем за 50 мс с Go.
Несколько советов по оптимизации производительности:
- Минимизируйте объем передаваемых данных между сервисами.
- Используйте пакетные операции при обработке нескольких элементов.
- Кэшируйте результаты, когда это уместно.
Мониторинг и отладка
Отладка распределенных систем может быть сложной. Мы добавили несколько инструментов, чтобы упростить процесс:
func (s *Service) call(method string, args interface{}) { start := time.Now() // Выполнение метода duration := time.Since(start) metrics.Timing("go_service."+method, duration) if duration > time.Second { log.Printf("Медленный вызов %s: %s", method, duration) } }
Что дальше
Я очень доволен тем, как работает эта архитектура. Мы планируем расширить наши сервисы на Go для обработки еще более ресурсоемких операций и улучшить наши инструменты разработки.
Большое спасибо команде Spiral Project за их библиотеку Goridge, которая сделала настройку RPC-коммуникации невероятно простой.
Примечание: Примеры кода в этой статье являются упрощенными версиями нашего рабочего кода, но они иллюстрируют основные концепции, которые мы используем.